Compare commits
234 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5c5b7ea43 | ||
|
|
06f1f3726d | ||
|
|
b1d5a7a1f8 | ||
|
|
166958364a | ||
|
|
16d1d871eb | ||
|
|
8354aaa3cc | ||
|
|
586c7daf2a | ||
|
|
12ae92b366 | ||
|
|
7160290aaf | ||
|
|
5ab82f83cb | ||
|
|
a13df8a1af | ||
|
|
3fe6f86659 | ||
|
|
ccf4a2de6e | ||
|
|
c2db8926df | ||
|
|
5b7d900424 | ||
|
|
c3402b0b12 | ||
|
|
32414451f5 | ||
|
|
a661e1cdb7 | ||
|
|
cdb48f510e | ||
|
|
c799261a72 | ||
|
|
8b32ace9e5 | ||
|
|
3812c0e56e | ||
|
|
cb1b23a5b2 | ||
|
|
d4f539cd7a | ||
|
|
609b4a56a9 | ||
|
|
0b11d88a62 | ||
|
|
37e68803b9 | ||
|
|
22ecfd267c | ||
|
|
4fbe06d121 | ||
|
|
58bac49f97 | ||
|
|
f8fdcd7ae9 | ||
|
|
66bc5a0e65 | ||
|
|
e175f4fda3 | ||
|
|
a1e513b35d | ||
|
|
4d5b288116 | ||
|
|
362c947df1 | ||
|
|
209248d96d | ||
|
|
104d273ba5 | ||
|
|
dce3f02f66 | ||
|
|
e73bef58a3 | ||
|
|
5412f10ff8 | ||
|
|
4081d460a2 | ||
|
|
30d7846122 | ||
|
|
1476676cec | ||
|
|
165d290374 | ||
|
|
9614a252c9 | ||
|
|
d302444650 | ||
|
|
1370f7e51a | ||
|
|
36d18312b0 | ||
|
|
7b333150a2 | ||
|
|
a20f321a19 | ||
|
|
525eca63d2 | ||
|
|
edd7cf2434 | ||
|
|
f1a01597d9 | ||
|
|
16e4f5130b | ||
|
|
13d3567eb4 | ||
|
|
a846e936e9 | ||
|
|
d0e1107f36 | ||
|
|
42095a6da5 | ||
|
|
28ac43bd50 | ||
|
|
5e684fedbe | ||
|
|
086a3b01a1 | ||
|
|
d10fe36ec0 | ||
|
|
cd2a990df5 | ||
|
|
dc77bcdd5a | ||
|
|
c0ac18d53a | ||
|
|
14cb58df2b | ||
|
|
2e2c9ea286 | ||
|
|
82e4f3a345 | ||
|
|
9b60ed23e1 | ||
|
|
332d1cc318 | ||
|
|
8defe2eb3a | ||
|
|
830f55e538 | ||
|
|
c418306fd9 | ||
|
|
9053e3188d | ||
|
|
f0b19f30d1 | ||
|
|
666d569b48 | ||
|
|
646ea00db2 | ||
|
|
d4357197c9 | ||
|
|
4ce999c014 | ||
|
|
5fa1e8e3b1 | ||
|
|
5982d2285c | ||
|
|
b455268fa1 | ||
|
|
25c4a7d541 | ||
|
|
088d9a8e34 | ||
|
|
defd1f90b8 | ||
|
|
c9954af084 | ||
|
|
dc0ca22f5e | ||
|
|
c7f01f0795 | ||
|
|
37e45619ae | ||
|
|
72c5e24ab8 | ||
|
|
8e8e6d1b17 | ||
|
|
9667229429 | ||
|
|
33bac0f7c2 | ||
|
|
63c248440c | ||
|
|
71be32b245 | ||
|
|
4b19637fbb | ||
|
|
26f15b99bb | ||
|
|
f3c17b3de0 | ||
|
|
298ebc7c01 | ||
|
|
0735abdaad | ||
|
|
0dfe5cb44a | ||
|
|
4ccdd2baf4 | ||
|
|
e4ce97b887 | ||
|
|
aaeca70b4c | ||
|
|
8e8979af60 | ||
|
|
725c300db2 | ||
|
|
f3f69b300f | ||
|
|
c926884049 | ||
|
|
0e99adc72e | ||
|
|
c444544a27 | ||
|
|
84c24b344f | ||
|
|
34d4131968 | ||
|
|
d3f25dd6ea | ||
|
|
f3c29bd545 | ||
|
|
ea1f95b4ed | ||
|
|
42d611828a | ||
|
|
9388002158 | ||
|
|
1d4dcddc11 | ||
|
|
3ee47efff7 | ||
|
|
fe27e39c5d | ||
|
|
57aadceb17 | ||
|
|
5484931892 | ||
|
|
b5db5ffe54 | ||
|
|
9312c1807b | ||
|
|
7680505eed | ||
|
|
d15a68514d | ||
|
|
53ed5b8481 | ||
|
|
c89f2f3819 | ||
|
|
ebecb2a7f6 | ||
|
|
4995acc825 | ||
|
|
a4a61fcf50 | ||
|
|
86997bc0ac | ||
|
|
a740e819ae | ||
|
|
c7815939d2 | ||
|
|
2c8d5a19ac | ||
|
|
6b5d9f0fe5 | ||
|
|
ea7c68997f | ||
|
|
115a78d5f5 | ||
|
|
1929e3a80a | ||
|
|
7546d18a40 | ||
|
|
83915e8421 | ||
|
|
0493bbf1d1 | ||
|
|
41f8c9cd00 | ||
|
|
3d4df8807d | ||
|
|
850bd76aee | ||
|
|
7f294abfbb | ||
|
|
3ea7d8c9b6 | ||
|
|
06ad211fce | ||
|
|
84e53f635c | ||
|
|
f6c4d49b9f | ||
|
|
06f2845ac0 | ||
|
|
a3b14a97d6 | ||
|
|
0723936b8a | ||
|
|
0d7727a7d4 | ||
|
|
79fd004346 | ||
|
|
70142f3705 | ||
|
|
c42f80c280 | ||
|
|
a7feab605b | ||
|
|
51966d52d5 | ||
|
|
20d47ecaa0 | ||
|
|
d85ce0a6dd | ||
|
|
5593208e61 | ||
|
|
beb1233358 | ||
|
|
75d649578a | ||
|
|
bd98031036 | ||
|
|
687f03f047 | ||
|
|
3dfcbfe136 | ||
|
|
2523916ca1 | ||
|
|
7ffcbe57a7 | ||
|
|
7b4fc46369 | ||
|
|
09439a10ce | ||
|
|
c93c1a8097 | ||
|
|
1ae4f7aa13 | ||
|
|
a11284f0f5 | ||
|
|
ad108f519a | ||
|
|
9787794ea1 | ||
|
|
41dd68903c | ||
|
|
f1834633b1 | ||
|
|
1e6032e01a | ||
|
|
059880f51a | ||
|
|
1d94ac85a4 | ||
|
|
d6ce4c237a | ||
|
|
4c314cf570 | ||
|
|
d520fa7774 | ||
|
|
65213e97d6 | ||
|
|
b921b85665 | ||
|
|
41c38a4b53 | ||
|
|
da945a12be | ||
|
|
51bf550d12 | ||
|
|
1f17005edd | ||
|
|
4204bc7e78 | ||
|
|
385fc37b1d | ||
|
|
b62b161b95 | ||
|
|
9def059e29 | ||
|
|
fb276e7a4a | ||
|
|
543fa264b3 | ||
|
|
cf235ceb4e | ||
|
|
4517022f36 | ||
|
|
08ba07d676 | ||
|
|
023e22c9d3 | ||
|
|
549cdc7222 | ||
|
|
4daade7366 | ||
|
|
a949998664 | ||
|
|
aa208a2d30 | ||
|
|
99fb4c8a5f | ||
|
|
5f994a83d8 | ||
|
|
623736d640 | ||
|
|
3318874da1 | ||
|
|
6f6829bb91 | ||
|
|
ab2f1749bf | ||
|
|
232b3b7ac6 | ||
|
|
67e62a2f21 | ||
|
|
473dedb9ad | ||
|
|
e5e391db38 | ||
|
|
4209ed7599 | ||
|
|
763ef2224b | ||
|
|
769cf8fac7 | ||
|
|
1f4b369912 | ||
|
|
7c97d8ede9 | ||
|
|
5a79234677 | ||
|
|
23732187a9 | ||
|
|
5e79f21913 | ||
|
|
5dbe973701 | ||
|
|
8e9563a440 | ||
|
|
566064ba9a | ||
|
|
7f095e0a6f | ||
|
|
552751bd93 | ||
|
|
f3cb8f758c | ||
|
|
683dad1443 | ||
|
|
4688b0f879 | ||
|
|
c37ecd747f | ||
|
|
6ddb73d54f | ||
|
|
5e0fe9c862 |
@@ -1,7 +1,7 @@
|
||||
= Contributing to Spring Authorization Server
|
||||
|
||||
Spring Authorization Server is released under the Apache 2.0 license.
|
||||
If you would like to contribute something, or simply want to hack on the code this document should help you https://github.com/spring-projects-experimental/spring-authorization-server#getting-started[get started].
|
||||
If you would like to contribute something, or simply want to hack on the code this document should help you https://github.com/spring-projects/spring-authorization-server#getting-started[get started].
|
||||
|
||||
== Code of Conduct
|
||||
This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct].
|
||||
@@ -32,7 +32,7 @@ That may mean using an external library directly in a `Filter`.
|
||||
|
||||
== Reporting Security Vulnerabilities
|
||||
If you think you have found a security vulnerability please *DO NOT* disclose it publicly until we've had a chance to fix it.
|
||||
Please don't report security vulnerabilities using GitHub issues, instead head over to https://pivotal.io/security and learn how to disclose them responsibly.
|
||||
Please don't report security vulnerabilities using GitHub issues, instead head over to https://spring.io/security-policy and learn how to disclose them responsibly.
|
||||
|
||||
== Sign the Contributor License Agreement
|
||||
Before we accept a non-trivial patch or pull request we will need you to https://cla.pivotal.io/sign/spring[sign the Contributor License Agreement].
|
||||
@@ -45,7 +45,7 @@ Please add the Apache License header to all new classes, for example:
|
||||
|
||||
```java
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
9
Jenkinsfile
vendored
9
Jenkinsfile
vendored
@@ -15,10 +15,11 @@ def GRADLE_ENTERPRISE_SECRET_ACCESS_KEY = string(credentialsId: 'gradle_enterpri
|
||||
variable: 'GRADLE_ENTERPRISE_ACCESS_KEY')
|
||||
def SPRING_SIGNING_SECRING = file(credentialsId: 'spring-signing-secring.gpg', variable: 'SIGNING_KEYRING_FILE')
|
||||
def SPRING_GPG_PASSPHRASE = string(credentialsId: 'spring-gpg-passphrase', variable: 'SIGNING_PASSWORD')
|
||||
def OSSRH_CREDENTIALS = usernamePassword(credentialsId: 'oss-token', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USERNAME')
|
||||
def OSSRH_S01_CREDENTIALS = usernamePassword(credentialsId: 'oss-s01-token', passwordVariable: 'OSSRH_S01_TOKEN_PASSWORD', usernameVariable: 'OSSRH_S01_TOKEN_USERNAME')
|
||||
def ARTIFACTORY_CREDENTIALS = usernamePassword(credentialsId: '02bd1690-b54f-4c9f-819d-a77cb7a9822c', usernameVariable: 'ARTIFACTORY_USERNAME', passwordVariable: 'ARTIFACTORY_PASSWORD')
|
||||
def JENKINS_PRIVATE_SSH_KEY = file(credentialsId: 'docs.spring.io-jenkins_private_ssh_key', variable: 'DEPLOY_SSH_KEY')
|
||||
def SONAR_LOGIN_CREDENTIALS = string(credentialsId: 'spring-sonar.login', variable: 'SONAR_LOGIN')
|
||||
def JENKINS_USER = '-Duser.name="spring-builds+jenkins"'
|
||||
|
||||
def jdkEnv(String jdk = 'jdk8') {
|
||||
def jdkTool = tool(jdk)
|
||||
@@ -39,7 +40,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 check -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --stacktrace"
|
||||
sh "./gradlew $JENKINS_USER check -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --stacktrace"
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
@@ -60,7 +61,7 @@ try {
|
||||
sh "git clean -dfx"
|
||||
withCredentials([SPRING_SIGNING_SECRING,
|
||||
SPRING_GPG_PASSPHRASE,
|
||||
OSSRH_CREDENTIALS,
|
||||
OSSRH_S01_CREDENTIALS,
|
||||
ARTIFACTORY_CREDENTIALS,
|
||||
GRADLE_ENTERPRISE_CACHE_USER,
|
||||
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY]) {
|
||||
@@ -68,7 +69,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 deployArtifacts finalizeDeployArtifacts -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password='$SIGNING_PASSWORD' -PossrhUsername=$OSSRH_USERNAME -PossrhPassword=$OSSRH_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --stacktrace"
|
||||
sh "./gradlew $JENKINS_USER deployArtifacts finalizeDeployArtifacts -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password='$SIGNING_PASSWORD' -PossrhTokenUsername=$OSSRH_S01_TOKEN_USERNAME -PossrhTokenPassword=$OSSRH_S01_TOKEN_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --stacktrace"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
README.adoc
23
README.adoc
@@ -1,27 +1,26 @@
|
||||
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://github.com/spring-projects-experimental/spring-authorization-server/workflows/CI/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects-experimental/spring-authorization-server/actions?query=workflow%3ACI"]
|
||||
image:https://github.com/spring-projects/spring-authorization-server/workflows/CI/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-authorization-server/actions?query=workflow%3ACI"]
|
||||
|
||||
= Spring Authorization Server
|
||||
|
||||
Spring Authorization Server is a community-driven project led by the https://spring.io/projects/spring-security/[Spring Security] team and is focused on delivering https://tools.ietf.org/html/rfc6749#section-1.1[OAuth 2.0 Authorization Server] support to the Spring community.
|
||||
The Spring Authorization Server project, led by the https://spring.io/projects/spring-security/[Spring Security] team, is focused on delivering https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-1.1[OAuth 2.1 Authorization Server] support to the Spring community.
|
||||
|
||||
The project will start in Spring's experimental projects as an independent project so that it can evolve more rapidly.
|
||||
|
||||
The ultimate goal of this project is to replace the Authorization Server support provided by https://spring.io/projects/spring-security-oauth/[Spring Security OAuth].
|
||||
|
||||
With the much needed help from our community, this project will grow in the same way that the original Spring Security OAuth project did.
|
||||
This project replaces the Authorization Server support provided by https://spring.io/projects/spring-security-oauth/[Spring Security OAuth].
|
||||
|
||||
== Feature Planning
|
||||
This project uses https://www.zenhub.com/[ZenHub] to prioritize the feature roadmap and help organize the project plan.
|
||||
The project board can be accessed https://app.zenhub.com/workspaces/authorization-server-5e8f3182b5e8f5841bfc4902/board?repos=248032165[here].
|
||||
It is recommended to install the ZenHub https://www.zenhub.com/extension[browser extension] as it integrates natively within GitHub's user interface.
|
||||
|
||||
The completed and upcoming feature list can be viewed in the https://github.com/spring-projects-experimental/spring-authorization-server/wiki/Feature-List[wiki].
|
||||
The completed and upcoming feature list can be viewed in the https://github.com/spring-projects/spring-authorization-server/wiki/Feature-List[wiki].
|
||||
|
||||
== Support Policy
|
||||
The Spring Authorization Server project provides software support and is documented in its link:SUPPORT_POLICY.adoc[support policy].
|
||||
|
||||
== Getting Started
|
||||
The first place to start is to read the https://tools.ietf.org/html/rfc6749[OAuth 2.0 Authorization Framework] to gain an in-depth understanding on how to build an Authorization Server.
|
||||
It is a critically important first step as the implementation must conform to the specification defined in the OAuth 2.0 Authorization Framework and the https://github.com/spring-projects-experimental/spring-authorization-server/wiki/OAuth-2.0-Specifications[related specifications].
|
||||
The first place to start is to read the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01[OAuth 2.1 Authorization Framework] to gain an in-depth understanding on how to build an Authorization Server.
|
||||
It is a critically important first step as the implementation must conform to the specification defined in the OAuth 2.1 Authorization Framework and the https://github.com/spring-projects/spring-authorization-server/wiki/OAuth-2.0-Specifications[related specifications].
|
||||
|
||||
The second place to start is to become very familiar with the codebase in the following Spring Security modules:
|
||||
|
||||
@@ -36,7 +35,7 @@ The goal is to leverage all the knowledge learned thus far and apply the same to
|
||||
Submitted work via pull requests should follow the same coding style/conventions and adopt the same or similar design patterns that have been established in Spring Security's OAuth 2.0 support.
|
||||
|
||||
== Documentation
|
||||
Be sure to read the https://docs.spring.io/spring-security/site/docs/current/reference/html5/[Spring Security Reference], as well as the https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2[OAuth 2.0 Reference], which describes the Client and Resource Server features available.
|
||||
Be sure to read the https://docs.spring.io/spring-security/reference[Spring Security Reference], as well as the https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html[OAuth 2.0 Reference], which describes the Client and Resource Server features available.
|
||||
|
||||
Extensive JavaDoc for the Spring Security code is also available in the https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API Documentation].
|
||||
|
||||
@@ -60,7 +59,7 @@ Be sure that your `JAVA_HOME` environment variable points to the `jdk1.8.0` fold
|
||||
=== Check out sources
|
||||
[indent=0]
|
||||
----
|
||||
git clone git@github.com:spring-projects-experimental/spring-authorization-server.git
|
||||
git clone git@github.com:spring-projects/spring-authorization-server.git
|
||||
|
||||
----
|
||||
|
||||
|
||||
21
SUPPORT_POLICY.adoc
Normal file
21
SUPPORT_POLICY.adoc
Normal file
@@ -0,0 +1,21 @@
|
||||
= Spring Authorization Server Support Policy
|
||||
|
||||
The Spring Authorization Server support offering provides the following support terms:
|
||||
|
||||
* Releases are currently in the format of 0.x.y, where:
|
||||
** “x” contains new features and potentially breaking changes.
|
||||
** “y” contains new features and bug fixes and provides backward compatibility.
|
||||
* The Spring Authorization Server project will be supported for at least 3 years after the most recent 0.x.0 release is made available for download.
|
||||
* Security fixes will be provided for at least one year after the 0.x.0 release is made available for download. Security fixes will not be provided for updating versions to third-party libraries.
|
||||
* Feature support and bug fixes, excluding “Security fixes”, will be provided only for the latest 0.x.y release.
|
||||
* This support policy starts with version 0.2.0.
|
||||
* We will switch to the standard https://tanzu.vmware.com/support/oss[Spring OSS support policy] when the Spring Authorization Server project reaches version 1.0.0.
|
||||
|
||||
An example can help us understand all of these points.
|
||||
Assume that 0.2.0 is released in August of 2021.
|
||||
This means that the Spring Authorization Server project is supported until at least August of 2024.
|
||||
If 0.3.0 is then released in May of 2022, the Spring Authorization Server project is supported until at least May of 2025.
|
||||
The 0.3.0 release may contain breaking changes from 0.2.0.
|
||||
If a bug is found, only 0.3.0 will be patched in a 0.3.1 release.
|
||||
If a security vulnerability is found, a 0.2.4 (assume 0.2.3 is latest) and 0.3.1 release will be provided to fix the security vulnerability.
|
||||
However, a vulnerability found in September of 2022 would be fixed in the 0.3.1 release but not the 0.2.3 release, because the vulnerability was discovered more than a year after the 0.2.0 release date.
|
||||
@@ -1,8 +1,8 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.37'
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.38'
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.5.RELEASE'
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.8'
|
||||
}
|
||||
repositories {
|
||||
maven {
|
||||
@@ -22,7 +22,7 @@ apply plugin: 'io.spring.nohttp'
|
||||
apply plugin: 'locks'
|
||||
apply plugin: 'io.spring.convention.root'
|
||||
|
||||
group = 'org.springframework.security.experimental'
|
||||
group = 'org.springframework.security'
|
||||
description = 'Spring Authorization Server'
|
||||
|
||||
ext.snapshotBuild = version.contains("SNAPSHOT")
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
apply plugin: 'io.spring.convention.docs'
|
||||
apply plugin: 'io.spring.convention.springdependencymangement'
|
||||
apply plugin: 'io.spring.convention.dependency-set'
|
||||
apply plugin: 'io.spring.convention.repository'
|
||||
apply plugin: 'java'
|
||||
|
||||
asciidoctor {
|
||||
attributes([stylesheet: 'css/style.css'])
|
||||
resources {
|
||||
from(sourceDir) {
|
||||
include "css/**"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
asciidoctorj {
|
||||
def ghTag = snapshotBuild ? 'main' : project.version
|
||||
def ghUrl = "https://github.com/spring-projects-experimental/spring-authorization-server/tree/$ghTag"
|
||||
attributes 'spring-authorization-server-version' : project.version,
|
||||
'spring-boot-version' : springBootVersion,
|
||||
revnumber : project.version,
|
||||
'gh-url': ghUrl,
|
||||
'gh-samples-url': "$ghUrl/samples"
|
||||
attributeProvider resolvedVersions(project.configurations.testCompile)
|
||||
}
|
||||
|
||||
def resolvedVersions(Configuration configuration) {
|
||||
return {
|
||||
configuration.resolvedConfiguration
|
||||
.resolvedArtifacts
|
||||
.collectEntries { [(it.name + "-version"): it.moduleVersion.id.version] }
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { url "https://repo.spring.io/release" }
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
@import 'spring.css';
|
||||
|
||||
a code {
|
||||
color: #097dff;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
= Spring Authorization Server Reference
|
||||
Joe Grandja
|
||||
:include-dir: _includes
|
||||
:security-api-url: https://docs.spring.io/spring-authorization-server/site/docs/current/api/
|
||||
:source-indent: 0
|
||||
:tabsize: 4
|
||||
:toc: left
|
||||
|
||||
== Preface
|
||||
|
||||
#TODO:# Document preface
|
||||
|
||||
== Introduction
|
||||
|
||||
#TODO:# Document introduction
|
||||
@@ -1,5 +1,5 @@
|
||||
version=0.1.1
|
||||
springBootVersion=2.4.5
|
||||
version=0.2.3
|
||||
springBootVersion=2.5.10
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
if (!project.hasProperty("springVersion")) {
|
||||
ext.springVersion = "5.3.6"
|
||||
ext.springVersion = "5.3.16"
|
||||
}
|
||||
|
||||
if (!project.hasProperty("springSecurityVersion")) {
|
||||
ext.springSecurityVersion = "5.4.6"
|
||||
ext.springSecurityVersion = "5.5.5"
|
||||
}
|
||||
|
||||
if (!project.hasProperty("reactorVersion")) {
|
||||
ext.reactorVersion = "2020.0.6"
|
||||
ext.reactorVersion = "2020.0.16"
|
||||
}
|
||||
|
||||
if (!project.hasProperty("locksDisabled")) {
|
||||
@@ -21,16 +21,18 @@ dependencyManagement {
|
||||
mavenBom "org.springframework:spring-framework-bom:$springVersion"
|
||||
mavenBom "org.springframework.security:spring-security-bom:$springSecurityVersion"
|
||||
mavenBom "io.projectreactor:reactor-bom:$reactorVersion"
|
||||
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.0"
|
||||
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.6"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
dependency "com.nimbusds:nimbus-jose-jwt:9.10.1"
|
||||
dependency "javax.servlet:javax.servlet-api:4.0.1"
|
||||
dependency 'junit:junit:4.13.1'
|
||||
dependency 'org.assertj:assertj-core:3.18.1'
|
||||
dependency 'org.mockito:mockito-core:3.6.28'
|
||||
dependency 'junit:junit:4.13.2'
|
||||
dependency 'org.assertj:assertj-core:3.19.0'
|
||||
dependency 'org.mockito:mockito-core:3.9.0'
|
||||
dependency "com.squareup.okhttp3:mockwebserver:3.14.9"
|
||||
dependency "com.squareup.okhttp3:okhttp:3.14.9"
|
||||
dependency "com.jayway.jsonpath:json-path:2.4.0"
|
||||
dependency "com.jayway.jsonpath:json-path:2.5.0"
|
||||
dependency "org.hsqldb:hsqldb:2.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -10,12 +10,18 @@ dependencies {
|
||||
compile 'com.nimbusds:nimbus-jose-jwt'
|
||||
compile 'com.fasterxml.jackson.core:jackson-databind'
|
||||
|
||||
optional 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||
optional 'org.springframework:spring-jdbc'
|
||||
|
||||
testCompile 'org.springframework.security:spring-security-test'
|
||||
testCompile 'org.springframework:spring-webmvc'
|
||||
testCompile 'junit:junit'
|
||||
testCompile 'org.assertj:assertj-core'
|
||||
testCompile 'org.mockito:mockito-core'
|
||||
testCompile 'com.jayway.jsonpath:json-path'
|
||||
testCompile 'com.squareup.okhttp3:mockwebserver'
|
||||
|
||||
testRuntime 'org.hsqldb:hsqldb'
|
||||
|
||||
provided 'javax.servlet:javax.servlet-api'
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
@@ -68,12 +68,10 @@ public class OAuth2AuthorizationServerConfiguration {
|
||||
authorizeRequests.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
|
||||
.apply(authorizationServerConfigurer);
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
@Bean
|
||||
public static JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||
Set<JWSAlgorithm> jwsAlgs = new HashSet<>();
|
||||
jwsAlgs.addAll(JWSAlgorithm.Family.RSA);
|
||||
@@ -89,4 +87,11 @@ public class OAuth2AuthorizationServerConfiguration {
|
||||
return new NimbusJwtDecoder(jwtProcessor);
|
||||
}
|
||||
|
||||
@Bean
|
||||
RegisterMissingBeanPostProcessor registerMissingBeanPostProcessor() {
|
||||
RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor();
|
||||
postProcessor.addBeanDefinition(ProviderSettings.class, () -> ProviderSettings.builder().build());
|
||||
return postProcessor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.config.annotation.web.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
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.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
|
||||
|
||||
/**
|
||||
* Post processor to register one or more bean definitions on container initialization, if not already present.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.2.0
|
||||
*/
|
||||
final class RegisterMissingBeanPostProcessor implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
|
||||
private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
|
||||
private final List<AbstractBeanDefinition> beanDefinitions = new ArrayList<>();
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
|
||||
for (AbstractBeanDefinition beanDefinition : this.beanDefinitions) {
|
||||
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
|
||||
(ListableBeanFactory) this.beanFactory, beanDefinition.getBeanClass(), false, false);
|
||||
if (beanNames.length == 0) {
|
||||
String beanName = this.beanNameGenerator.generateBeanName(beanDefinition, registry);
|
||||
registry.registerBeanDefinition(beanName, beanDefinition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||
}
|
||||
|
||||
<T> void addBeanDefinition(Class<T> beanClass, Supplier<T> beanSupplier) {
|
||||
this.beanDefinitions.add(new RootBeanDefinition(beanClass, beanSupplier));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
/**
|
||||
* Base configurer for an OAuth 2.0 component (e.g. protocol endpoint).
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
*/
|
||||
abstract class AbstractOAuth2Configurer {
|
||||
private final ObjectPostProcessor<Object> objectPostProcessor;
|
||||
|
||||
AbstractOAuth2Configurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
this.objectPostProcessor = objectPostProcessor;
|
||||
}
|
||||
|
||||
abstract <B extends HttpSecurityBuilder<B>> void init(B builder);
|
||||
|
||||
abstract <B extends HttpSecurityBuilder<B>> void configure(B builder);
|
||||
|
||||
abstract RequestMatcher getRequestMatcher();
|
||||
|
||||
protected final <T> T postProcess(T object) {
|
||||
return (T) this.objectPostProcessor.postProcess(object);
|
||||
}
|
||||
|
||||
protected final ObjectPostProcessor<Object> getObjectPostProcessor() {
|
||||
return this.objectPostProcessor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Configurer for the OAuth 2.0 Authorization Endpoint.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationServerConfigurer#authorizationEndpoint
|
||||
* @see OAuth2AuthorizationEndpointFilter
|
||||
*/
|
||||
public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private AuthenticationConverter authorizationRequestConverter;
|
||||
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
private AuthenticationSuccessHandler authorizationResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
private String consentPage;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OAuth2AuthorizationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @param authorizationRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationEndpointConfigurer authorizationRequestConverter(AuthenticationConverter authorizationRequestConverter) {
|
||||
this.authorizationRequestConverter = authorizationRequestConverter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link AuthenticationProvider} used for authenticating an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
*
|
||||
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
|
||||
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
|
||||
this.authenticationProviders.add(authenticationProvider);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
* and returning the {@link OAuth2AuthorizationResponse Authorization Response}.
|
||||
*
|
||||
* @param authorizationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationEndpointConfigurer authorizationResponseHandler(AuthenticationSuccessHandler authorizationResponseHandler) {
|
||||
this.authorizationResponseHandler = authorizationResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
|
||||
* and returning the {@link OAuth2Error Error Response}.
|
||||
*
|
||||
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
|
||||
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
|
||||
this.errorResponseHandler = errorResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the URI to redirect Resource Owners to if consent is required during
|
||||
* the {@code authorization_code} flow. A default consent page will be generated when
|
||||
* this attribute is not specified.
|
||||
*
|
||||
* If a URI is specified, applications are required to process the specified URI to generate
|
||||
* a consent page. The query string will contain the following parameters:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code client_id} - the client identifier</li>
|
||||
* <li>{@code scope} - a space-delimited list of scopes present in the authorization request</li>
|
||||
* <li>{@code state} - a CSRF protection token</li>
|
||||
* </ul>
|
||||
*
|
||||
* In general, the consent page should create a form that submits
|
||||
* a request with the following requirements:
|
||||
*
|
||||
* <ul>
|
||||
* <li>It must be an HTTP POST</li>
|
||||
* <li>It must be submitted to {@link ProviderSettings#getAuthorizationEndpoint()} ()}</li>
|
||||
* <li>It must include the received {@code client_id} as an HTTP parameter</li>
|
||||
* <li>It must include the received {@code state} as an HTTP parameter</li>
|
||||
* <li>It must include the list of {@code scope}s the {@code Resource Owner}
|
||||
* consented to as an HTTP parameter</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent")
|
||||
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationEndpointConfigurer consentPage(String consentPage) {
|
||||
this.consentPage = consentPage;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void init(B builder) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
this.requestMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.getAuthorizationEndpoint(),
|
||||
HttpMethod.GET.name()),
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.getAuthorizationEndpoint(),
|
||||
HttpMethod.POST.name()));
|
||||
|
||||
List<AuthenticationProvider> authenticationProviders =
|
||||
!this.authenticationProviders.isEmpty() ?
|
||||
this.authenticationProviders :
|
||||
createDefaultAuthenticationProviders(builder);
|
||||
authenticationProviders.forEach(authenticationProvider ->
|
||||
builder.authenticationProvider(postProcess(authenticationProvider)));
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
|
||||
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
|
||||
new OAuth2AuthorizationEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.getAuthorizationEndpoint());
|
||||
if (this.authorizationRequestConverter != null) {
|
||||
authorizationEndpointFilter.setAuthenticationConverter(this.authorizationRequestConverter);
|
||||
}
|
||||
if (this.authorizationResponseHandler != null) {
|
||||
authorizationEndpointFilter.setAuthenticationSuccessHandler(this.authorizationResponseHandler);
|
||||
}
|
||||
if (this.errorResponseHandler != null) {
|
||||
authorizationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
|
||||
}
|
||||
if (StringUtils.hasText(this.consentPage)) {
|
||||
authorizationEndpointFilter.setConsentPage(this.consentPage);
|
||||
}
|
||||
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
RequestMatcher getRequestMatcher() {
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(builder),
|
||||
OAuth2ConfigurerUtils.getAuthorizationConsentService(builder));
|
||||
authenticationProviders.add(authorizationCodeRequestAuthenticationProvider);
|
||||
|
||||
return authenticationProviders;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,55 +16,50 @@
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import javax.servlet.AsyncContext;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.Transient;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.oauth2.server.authorization.web.ProviderContextFilter;
|
||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.context.HttpRequestResponseHolder;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper;
|
||||
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support.
|
||||
@@ -73,40 +68,35 @@ import org.springframework.util.StringUtils;
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @author Gerardo Roza
|
||||
* @author Ovidiu Popa
|
||||
* @author Gaurav Tiwari
|
||||
* @since 0.0.1
|
||||
* @see AbstractHttpConfigurer
|
||||
* @see OAuth2ClientAuthenticationConfigurer
|
||||
* @see OAuth2AuthorizationEndpointConfigurer
|
||||
* @see OAuth2TokenEndpointConfigurer
|
||||
* @see OAuth2TokenIntrospectionEndpointConfigurer
|
||||
* @see OAuth2TokenRevocationEndpointConfigurer
|
||||
* @see OidcConfigurer
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2AuthorizationEndpointFilter
|
||||
* @see OAuth2TokenEndpointFilter
|
||||
* @see OAuth2TokenIntrospectionEndpointFilter
|
||||
* @see OAuth2TokenRevocationEndpointFilter
|
||||
* @see OAuth2AuthorizationConsentService
|
||||
* @see NimbusJwkSetEndpointFilter
|
||||
* @see OidcProviderConfigurationEndpointFilter
|
||||
* @see OAuth2AuthorizationServerMetadataEndpointFilter
|
||||
* @see OAuth2ClientAuthenticationFilter
|
||||
* @see OidcClientRegistrationEndpointFilter
|
||||
*/
|
||||
public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer<B>, B> {
|
||||
|
||||
private RequestMatcher authorizationEndpointMatcher;
|
||||
private RequestMatcher tokenEndpointMatcher;
|
||||
private RequestMatcher tokenIntrospectionEndpointMatcher;
|
||||
private RequestMatcher tokenRevocationEndpointMatcher;
|
||||
private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = createConfigurers();
|
||||
private RequestMatcher jwkSetEndpointMatcher;
|
||||
private RequestMatcher oidcProviderConfigurationEndpointMatcher;
|
||||
private RequestMatcher authorizationServerMetadataEndpointMatcher;
|
||||
private RequestMatcher oidcClientRegistrationEndpointMatcher;
|
||||
private final RequestMatcher endpointsMatcher = (request) ->
|
||||
this.authorizationEndpointMatcher.matches(request) ||
|
||||
this.tokenEndpointMatcher.matches(request) ||
|
||||
this.tokenIntrospectionEndpointMatcher.matches(request) ||
|
||||
this.tokenRevocationEndpointMatcher.matches(request) ||
|
||||
getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) ||
|
||||
getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) ||
|
||||
getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class).matches(request) ||
|
||||
getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) ||
|
||||
getRequestMatcher(OidcConfigurer.class).matches(request) ||
|
||||
this.jwkSetEndpointMatcher.matches(request) ||
|
||||
this.oidcProviderConfigurationEndpointMatcher.matches(request) ||
|
||||
this.authorizationServerMetadataEndpointMatcher.matches(request) ||
|
||||
this.oidcClientRegistrationEndpointMatcher.matches(request);
|
||||
this.authorizationServerMetadataEndpointMatcher.matches(request);
|
||||
|
||||
/**
|
||||
* Sets the repository of registered clients.
|
||||
@@ -116,7 +106,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> registeredClientRepository(RegisteredClientRepository registeredClientRepository) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
this.getBuilder().setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
|
||||
getBuilder().setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -128,7 +118,19 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> authorizationService(OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.getBuilder().setSharedObject(OAuth2AuthorizationService.class, authorizationService);
|
||||
getBuilder().setSharedObject(OAuth2AuthorizationService.class, authorizationService);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authorization consent service.
|
||||
*
|
||||
* @param authorizationConsentService the authorization consent service
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> authorizationConsentService(OAuth2AuthorizationConsentService authorizationConsentService) {
|
||||
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
|
||||
getBuilder().setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -140,7 +142,88 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings providerSettings) {
|
||||
Assert.notNull(providerSettings, "providerSettings cannot be null");
|
||||
this.getBuilder().setSharedObject(ProviderSettings.class, providerSettings);
|
||||
getBuilder().setSharedObject(ProviderSettings.class, providerSettings);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the token generator.
|
||||
*
|
||||
* @param tokenGenerator the token generator
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> tokenGenerator(OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
getBuilder().setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OAuth 2.0 Client Authentication.
|
||||
*
|
||||
* @param clientAuthenticationCustomizer the {@link Customizer} providing access to the {@link OAuth2ClientAuthenticationConfigurer}
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> clientAuthentication(Customizer<OAuth2ClientAuthenticationConfigurer> clientAuthenticationCustomizer) {
|
||||
clientAuthenticationCustomizer.customize(getConfigurer(OAuth2ClientAuthenticationConfigurer.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OAuth 2.0 Authorization Endpoint.
|
||||
*
|
||||
* @param authorizationEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2AuthorizationEndpointConfigurer}
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> authorizationEndpoint(Customizer<OAuth2AuthorizationEndpointConfigurer> authorizationEndpointCustomizer) {
|
||||
authorizationEndpointCustomizer.customize(getConfigurer(OAuth2AuthorizationEndpointConfigurer.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OAuth 2.0 Token Endpoint.
|
||||
*
|
||||
* @param tokenEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2TokenEndpointConfigurer}
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> tokenEndpoint(Customizer<OAuth2TokenEndpointConfigurer> tokenEndpointCustomizer) {
|
||||
tokenEndpointCustomizer.customize(getConfigurer(OAuth2TokenEndpointConfigurer.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OAuth 2.0 Token Introspection Endpoint.
|
||||
*
|
||||
* @param tokenIntrospectionEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2TokenIntrospectionEndpointConfigurer}
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> tokenIntrospectionEndpoint(Customizer<OAuth2TokenIntrospectionEndpointConfigurer> tokenIntrospectionEndpointCustomizer) {
|
||||
tokenIntrospectionEndpointCustomizer.customize(getConfigurer(OAuth2TokenIntrospectionEndpointConfigurer.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OAuth 2.0 Token Revocation Endpoint.
|
||||
*
|
||||
* @param tokenRevocationEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2TokenRevocationEndpointConfigurer}
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> tokenRevocationEndpoint(Customizer<OAuth2TokenRevocationEndpointConfigurer> tokenRevocationEndpointCustomizer) {
|
||||
tokenRevocationEndpointCustomizer.customize(getConfigurer(OAuth2TokenRevocationEndpointConfigurer.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OpenID Connect 1.0 support.
|
||||
*
|
||||
* @param oidcCustomizer the {@link Customizer} providing access to the {@link OidcConfigurer}
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> oidc(Customizer<OidcConfigurer> oidcCustomizer) {
|
||||
oidcCustomizer.customize(getConfigurer(OidcConfigurer.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -155,282 +238,201 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
|
||||
@Override
|
||||
public void init(B builder) {
|
||||
ProviderSettings providerSettings = getProviderSettings(builder);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
validateProviderSettings(providerSettings);
|
||||
initEndpointMatchers(providerSettings);
|
||||
|
||||
OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
|
||||
new OAuth2ClientAuthenticationProvider(
|
||||
getRegisteredClientRepository(builder),
|
||||
getAuthorizationService(builder));
|
||||
PasswordEncoder passwordEncoder = getOptionalBean(builder, PasswordEncoder.class);
|
||||
if (passwordEncoder != null) {
|
||||
clientAuthenticationProvider.setPasswordEncoder(passwordEncoder);
|
||||
}
|
||||
builder.authenticationProvider(postProcess(clientAuthenticationProvider));
|
||||
|
||||
JwtEncoder jwtEncoder = getJwtEncoder(builder);
|
||||
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(builder);
|
||||
|
||||
OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
|
||||
new OAuth2AuthorizationCodeAuthenticationProvider(
|
||||
getAuthorizationService(builder),
|
||||
jwtEncoder);
|
||||
if (jwtCustomizer != null) {
|
||||
authorizationCodeAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
|
||||
}
|
||||
builder.authenticationProvider(postProcess(authorizationCodeAuthenticationProvider));
|
||||
|
||||
OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
|
||||
new OAuth2RefreshTokenAuthenticationProvider(
|
||||
getAuthorizationService(builder),
|
||||
jwtEncoder);
|
||||
if (jwtCustomizer != null) {
|
||||
refreshTokenAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
|
||||
}
|
||||
builder.authenticationProvider(postProcess(refreshTokenAuthenticationProvider));
|
||||
|
||||
OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider =
|
||||
new OAuth2ClientCredentialsAuthenticationProvider(
|
||||
getAuthorizationService(builder),
|
||||
jwtEncoder);
|
||||
if (jwtCustomizer != null) {
|
||||
clientCredentialsAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
|
||||
}
|
||||
builder.authenticationProvider(postProcess(clientCredentialsAuthenticationProvider));
|
||||
|
||||
OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
|
||||
new OAuth2TokenIntrospectionAuthenticationProvider(
|
||||
getRegisteredClientRepository(builder),
|
||||
getAuthorizationService(builder));
|
||||
builder.authenticationProvider(postProcess(tokenIntrospectionAuthenticationProvider));
|
||||
|
||||
OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
|
||||
new OAuth2TokenRevocationAuthenticationProvider(
|
||||
getAuthorizationService(builder));
|
||||
builder.authenticationProvider(postProcess(tokenRevocationAuthenticationProvider));
|
||||
|
||||
// TODO Make OpenID Client Registration an "opt-in" feature
|
||||
OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider =
|
||||
new OidcClientRegistrationAuthenticationProvider(
|
||||
getRegisteredClientRepository(builder),
|
||||
getAuthorizationService(builder));
|
||||
builder.authenticationProvider(postProcess(oidcClientRegistrationAuthenticationProvider));
|
||||
this.configurers.values().forEach(configurer -> configurer.init(builder));
|
||||
|
||||
ExceptionHandlingConfigurer<B> exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptionHandling != null) {
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(
|
||||
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
|
||||
new OrRequestMatcher(
|
||||
this.tokenEndpointMatcher,
|
||||
this.tokenIntrospectionEndpointMatcher,
|
||||
this.tokenRevocationEndpointMatcher)
|
||||
getRequestMatcher(OAuth2TokenEndpointConfigurer.class),
|
||||
getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class),
|
||||
getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class))
|
||||
);
|
||||
}
|
||||
|
||||
// gh-482
|
||||
initSecurityContextRepository(builder);
|
||||
}
|
||||
|
||||
private void initSecurityContextRepository(B builder) {
|
||||
// TODO This is a temporary fix and should be removed after upgrading to Spring Security 5.7.0 GA.
|
||||
//
|
||||
// See:
|
||||
// Prevent Save @Transient Authentication with existing HttpSession
|
||||
// https://github.com/spring-projects/spring-security/pull/9993
|
||||
|
||||
final SecurityContextRepository securityContextRepository = builder.getSharedObject(SecurityContextRepository.class);
|
||||
if (!(securityContextRepository instanceof HttpSessionSecurityContextRepository)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SecurityContextRepository securityContextRepositoryTransientNotSaved = new SecurityContextRepository() {
|
||||
|
||||
private final RequestMatcher clientAuthenticationRequestMatcher = initClientAuthenticationRequestMatcher();
|
||||
private final RequestMatcher jwtAuthenticationRequestMatcher = initJwtAuthenticationRequestMatcher();
|
||||
|
||||
@Override
|
||||
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
|
||||
final HttpServletRequest unwrappedRequest = requestResponseHolder.getRequest();
|
||||
final HttpServletResponse unwrappedResponse = requestResponseHolder.getResponse();
|
||||
|
||||
SecurityContext securityContext = securityContextRepository.loadContext(requestResponseHolder);
|
||||
|
||||
if (this.clientAuthenticationRequestMatcher.matches(unwrappedRequest) ||
|
||||
this.jwtAuthenticationRequestMatcher.matches(unwrappedRequest)) {
|
||||
|
||||
final SaveContextOnUpdateOrErrorResponseWrapper transientAuthenticationResponseWrapper =
|
||||
new SaveContextOnUpdateOrErrorResponseWrapper(unwrappedResponse, false) {
|
||||
|
||||
@Override
|
||||
protected void saveContext(SecurityContext context) {
|
||||
// @Transient Authentication should not be saved
|
||||
if (context.getAuthentication() != null) {
|
||||
Assert.state(isTransientAuthentication(context.getAuthentication()), "Expected @Transient Authentication");
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
// Override the default HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper
|
||||
requestResponseHolder.setResponse(transientAuthenticationResponseWrapper);
|
||||
|
||||
final HttpServletRequestWrapper transientAuthenticationRequestWrapper =
|
||||
new HttpServletRequestWrapper(unwrappedRequest) {
|
||||
|
||||
@Override
|
||||
public AsyncContext startAsync() {
|
||||
transientAuthenticationResponseWrapper.disableSaveOnResponseCommitted();
|
||||
return super.startAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse)
|
||||
throws IllegalStateException {
|
||||
transientAuthenticationResponseWrapper.disableSaveOnResponseCommitted();
|
||||
return super.startAsync(servletRequest, servletResponse);
|
||||
}
|
||||
|
||||
};
|
||||
// Override the default HttpSessionSecurityContextRepository.SaveToSessionRequestWrapper
|
||||
requestResponseHolder.setRequest(transientAuthenticationRequestWrapper);
|
||||
}
|
||||
|
||||
return securityContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
|
||||
Authentication authentication = context.getAuthentication();
|
||||
if (authentication == null || isTransientAuthentication(authentication)) {
|
||||
return;
|
||||
}
|
||||
securityContextRepository.saveContext(context, request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsContext(HttpServletRequest request) {
|
||||
return securityContextRepository.containsContext(request);
|
||||
}
|
||||
|
||||
private boolean isTransientAuthentication(Authentication authentication) {
|
||||
return AnnotationUtils.getAnnotation(authentication.getClass(), Transient.class) != null;
|
||||
}
|
||||
|
||||
private RequestMatcher initClientAuthenticationRequestMatcher() {
|
||||
// OAuth2ClientAuthenticationToken is @Transient and is accepted by
|
||||
// OAuth2TokenEndpointFilter, OAuth2TokenIntrospectionEndpointFilter and OAuth2TokenRevocationEndpointFilter
|
||||
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
requestMatchers.add(getRequestMatcher(OAuth2TokenEndpointConfigurer.class));
|
||||
requestMatchers.add(getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class));
|
||||
requestMatchers.add(getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class));
|
||||
return new OrRequestMatcher(requestMatchers);
|
||||
}
|
||||
|
||||
private RequestMatcher initJwtAuthenticationRequestMatcher() {
|
||||
// JwtAuthenticationToken is @Transient and is accepted by
|
||||
// OidcUserInfoEndpointFilter and OidcClientRegistrationEndpointFilter
|
||||
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
requestMatchers.add(
|
||||
getConfigurer(OidcConfigurer.class)
|
||||
.getConfigurer(OidcUserInfoEndpointConfigurer.class).getRequestMatcher()
|
||||
);
|
||||
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
|
||||
getConfigurer(OidcConfigurer.class)
|
||||
.getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
|
||||
if (clientRegistrationEndpointConfigurer != null) {
|
||||
requestMatchers.add(clientRegistrationEndpointConfigurer.getRequestMatcher());
|
||||
}
|
||||
return new OrRequestMatcher(requestMatchers);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
builder.setSharedObject(SecurityContextRepository.class, securityContextRepositoryTransientNotSaved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(B builder) {
|
||||
ProviderSettings providerSettings = getProviderSettings(builder);
|
||||
if (providerSettings.issuer() != null) {
|
||||
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
|
||||
new OidcProviderConfigurationEndpointFilter(providerSettings);
|
||||
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
this.configurers.values().forEach(configurer -> configurer.configure(builder));
|
||||
|
||||
OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
|
||||
new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);
|
||||
builder.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
|
||||
ProviderContextFilter providerContextFilter = new ProviderContextFilter(providerSettings);
|
||||
builder.addFilterAfter(postProcess(providerContextFilter), SecurityContextPersistenceFilter.class);
|
||||
|
||||
JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(builder);
|
||||
if (jwkSource != null) {
|
||||
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
|
||||
jwkSource, providerSettings.getJwkSetEndpoint());
|
||||
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
}
|
||||
|
||||
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
|
||||
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
|
||||
jwkSource,
|
||||
providerSettings.jwkSetEndpoint());
|
||||
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
|
||||
new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);
|
||||
builder.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
}
|
||||
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
private Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> createConfigurers() {
|
||||
Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
|
||||
configurers.put(OAuth2ClientAuthenticationConfigurer.class, new OAuth2ClientAuthenticationConfigurer(this::postProcess));
|
||||
configurers.put(OAuth2AuthorizationEndpointConfigurer.class, new OAuth2AuthorizationEndpointConfigurer(this::postProcess));
|
||||
configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
|
||||
configurers.put(OAuth2TokenIntrospectionEndpointConfigurer.class, new OAuth2TokenIntrospectionEndpointConfigurer(this::postProcess));
|
||||
configurers.put(OAuth2TokenRevocationEndpointConfigurer.class, new OAuth2TokenRevocationEndpointConfigurer(this::postProcess));
|
||||
configurers.put(OidcConfigurer.class, new OidcConfigurer(this::postProcess));
|
||||
return configurers;
|
||||
}
|
||||
|
||||
OAuth2ClientAuthenticationFilter clientAuthenticationFilter =
|
||||
new OAuth2ClientAuthenticationFilter(
|
||||
authenticationManager,
|
||||
new OrRequestMatcher(
|
||||
this.tokenEndpointMatcher,
|
||||
this.tokenIntrospectionEndpointMatcher,
|
||||
this.tokenRevocationEndpointMatcher));
|
||||
builder.addFilterAfter(postProcess(clientAuthenticationFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T getConfigurer(Class<T> type) {
|
||||
return (T) this.configurers.get(type);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
|
||||
new OAuth2AuthorizationEndpointFilter(
|
||||
getRegisteredClientRepository(builder),
|
||||
getAuthorizationService(builder),
|
||||
providerSettings.authorizationEndpoint());
|
||||
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
|
||||
OAuth2TokenEndpointFilter tokenEndpointFilter =
|
||||
new OAuth2TokenEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.tokenEndpoint());
|
||||
builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
|
||||
|
||||
OAuth2TokenIntrospectionEndpointFilter tokenIntrospectionEndpointFilter =
|
||||
new OAuth2TokenIntrospectionEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.tokenIntrospectionEndpoint());
|
||||
builder.addFilterAfter(postProcess(tokenIntrospectionEndpointFilter), OAuth2TokenEndpointFilter.class);
|
||||
|
||||
OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
|
||||
new OAuth2TokenRevocationEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.tokenRevocationEndpoint());
|
||||
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenIntrospectionEndpointFilter.class);
|
||||
|
||||
// TODO Make OpenID Client Registration an "opt-in" feature
|
||||
OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter =
|
||||
new OidcClientRegistrationEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.oidcClientRegistrationEndpoint());
|
||||
builder.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), OAuth2TokenRevocationEndpointFilter.class);
|
||||
private <T extends AbstractOAuth2Configurer> RequestMatcher getRequestMatcher(Class<T> configurerType) {
|
||||
return getConfigurer(configurerType).getRequestMatcher();
|
||||
}
|
||||
|
||||
private void initEndpointMatchers(ProviderSettings providerSettings) {
|
||||
this.authorizationEndpointMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.authorizationEndpoint(),
|
||||
HttpMethod.GET.name()),
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.authorizationEndpoint(),
|
||||
HttpMethod.POST.name()));
|
||||
this.tokenEndpointMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.tokenEndpoint(), HttpMethod.POST.name());
|
||||
this.tokenIntrospectionEndpointMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.tokenIntrospectionEndpoint(), HttpMethod.POST.name());
|
||||
this.tokenRevocationEndpointMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.tokenRevocationEndpoint(), HttpMethod.POST.name());
|
||||
this.jwkSetEndpointMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.jwkSetEndpoint(), HttpMethod.GET.name());
|
||||
this.oidcProviderConfigurationEndpointMatcher = new AntPathRequestMatcher(
|
||||
OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
|
||||
providerSettings.getJwkSetEndpoint(), HttpMethod.GET.name());
|
||||
this.authorizationServerMetadataEndpointMatcher = new AntPathRequestMatcher(
|
||||
OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI, HttpMethod.GET.name());
|
||||
this.oidcClientRegistrationEndpointMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.oidcClientRegistrationEndpoint(), HttpMethod.POST.name());
|
||||
"/.well-known/oauth-authorization-server", HttpMethod.GET.name());
|
||||
}
|
||||
|
||||
private static void validateProviderSettings(ProviderSettings providerSettings) {
|
||||
if (providerSettings.issuer() != null) {
|
||||
if (providerSettings.getIssuer() != null) {
|
||||
try {
|
||||
new URI(providerSettings.issuer()).toURL();
|
||||
new URI(providerSettings.getIssuer()).toURL();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException("issuer must be a valid URL", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> RegisteredClientRepository getRegisteredClientRepository(B builder) {
|
||||
RegisteredClientRepository registeredClientRepository = builder.getSharedObject(RegisteredClientRepository.class);
|
||||
if (registeredClientRepository == null) {
|
||||
registeredClientRepository = getBean(builder, RegisteredClientRepository.class);
|
||||
builder.setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
|
||||
}
|
||||
return registeredClientRepository;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService getAuthorizationService(B builder) {
|
||||
OAuth2AuthorizationService authorizationService = builder.getSharedObject(OAuth2AuthorizationService.class);
|
||||
if (authorizationService == null) {
|
||||
authorizationService = getOptionalBean(builder, OAuth2AuthorizationService.class);
|
||||
if (authorizationService == null) {
|
||||
authorizationService = new InMemoryOAuth2AuthorizationService();
|
||||
}
|
||||
builder.setSharedObject(OAuth2AuthorizationService.class, authorizationService);
|
||||
}
|
||||
return authorizationService;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
|
||||
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
|
||||
if (jwtEncoder == null) {
|
||||
jwtEncoder = getOptionalBean(builder, JwtEncoder.class);
|
||||
if (jwtEncoder == null) {
|
||||
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
|
||||
jwtEncoder = new NimbusJwsEncoder(jwkSource);
|
||||
}
|
||||
builder.setSharedObject(JwtEncoder.class, jwtEncoder);
|
||||
}
|
||||
return jwtEncoder;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <B extends HttpSecurityBuilder<B>> JWKSource<SecurityContext> getJwkSource(B builder) {
|
||||
JWKSource<SecurityContext> jwkSource = builder.getSharedObject(JWKSource.class);
|
||||
if (jwkSource == null) {
|
||||
ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class);
|
||||
jwkSource = getBean(builder, type);
|
||||
builder.setSharedObject(JWKSource.class, jwkSource);
|
||||
}
|
||||
return jwkSource;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <B extends HttpSecurityBuilder<B>> OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(B builder) {
|
||||
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = builder.getSharedObject(OAuth2TokenCustomizer.class);
|
||||
if (jwtCustomizer == null) {
|
||||
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class);
|
||||
jwtCustomizer = getOptionalBean(builder, type);
|
||||
if (jwtCustomizer != null) {
|
||||
builder.setSharedObject(OAuth2TokenCustomizer.class, jwtCustomizer);
|
||||
}
|
||||
}
|
||||
return jwtCustomizer;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettings(B builder) {
|
||||
ProviderSettings providerSettings = builder.getSharedObject(ProviderSettings.class);
|
||||
if (providerSettings == null) {
|
||||
providerSettings = getOptionalBean(builder, ProviderSettings.class);
|
||||
if (providerSettings == null) {
|
||||
providerSettings = new ProviderSettings();
|
||||
}
|
||||
builder.setSharedObject(ProviderSettings.class, providerSettings);
|
||||
}
|
||||
return providerSettings;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>, T> T getBean(B builder, Class<T> type) {
|
||||
return builder.getSharedObject(ApplicationContext.class).getBean(type);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <B extends HttpSecurityBuilder<B>, T> T getBean(B builder, ResolvableType type) {
|
||||
ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
|
||||
String[] names = context.getBeanNamesForType(type);
|
||||
if (names.length == 1) {
|
||||
return (T) context.getBean(names[0]);
|
||||
}
|
||||
if (names.length > 1) {
|
||||
throw new NoUniqueBeanDefinitionException(type, names);
|
||||
}
|
||||
throw new NoSuchBeanDefinitionException(type);
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>, T> T getOptionalBean(B builder, Class<T> type) {
|
||||
Map<String, T> beansMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
|
||||
builder.getSharedObject(ApplicationContext.class), type);
|
||||
if (beansMap.size() > 1) {
|
||||
throw new NoUniqueBeanDefinitionException(type, beansMap.size(),
|
||||
"Expected single matching bean of type '" + type.getName() + "' but found " +
|
||||
beansMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(beansMap.keySet()));
|
||||
}
|
||||
return (!beansMap.isEmpty() ? beansMap.values().iterator().next() : null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <B extends HttpSecurityBuilder<B>, T> T getOptionalBean(B builder, ResolvableType type) {
|
||||
ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
|
||||
String[] names = context.getBeanNamesForType(type);
|
||||
if (names.length > 1) {
|
||||
throw new NoUniqueBeanDefinitionException(type, names);
|
||||
}
|
||||
return names.length == 1 ? (T) context.getBean(names[0]) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.JwtClientAssertionAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.PublicClientAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Configurer for OAuth 2.0 Client Authentication.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.0
|
||||
* @see OAuth2AuthorizationServerConfigurer#clientAuthentication
|
||||
* @see OAuth2ClientAuthenticationFilter
|
||||
*/
|
||||
public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private AuthenticationConverter authenticationConverter;
|
||||
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
private AuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OAuth2ClientAuthenticationConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2ClientAuthenticationToken} used for authenticating the client.
|
||||
*
|
||||
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
|
||||
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2ClientAuthenticationConfigurer authenticationConverter(AuthenticationConverter authenticationConverter) {
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link AuthenticationProvider} used for authenticating an {@link OAuth2ClientAuthenticationToken}.
|
||||
*
|
||||
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OAuth2ClientAuthenticationToken}
|
||||
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2ClientAuthenticationConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
|
||||
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
|
||||
this.authenticationProviders.add(authenticationProvider);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling a successful client authentication
|
||||
* and associating the {@link OAuth2ClientAuthenticationToken} to the {@link SecurityContext}.
|
||||
*
|
||||
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling a successful client authentication
|
||||
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2ClientAuthenticationConfigurer authenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||
this.authenticationSuccessHandler = authenticationSuccessHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationFailureHandler} used for handling a failed client authentication
|
||||
* and returning the {@link OAuth2Error Error Response}.
|
||||
*
|
||||
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling a failed client authentication
|
||||
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2ClientAuthenticationConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
|
||||
this.errorResponseHandler = errorResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void init(B builder) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
this.requestMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.getTokenEndpoint(),
|
||||
HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.getTokenIntrospectionEndpoint(),
|
||||
HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.getTokenRevocationEndpoint(),
|
||||
HttpMethod.POST.name()));
|
||||
|
||||
List<AuthenticationProvider> authenticationProviders =
|
||||
!this.authenticationProviders.isEmpty() ?
|
||||
this.authenticationProviders :
|
||||
createDefaultAuthenticationProviders(builder);
|
||||
authenticationProviders.forEach(authenticationProvider ->
|
||||
builder.authenticationProvider(postProcess(authenticationProvider)));
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
OAuth2ClientAuthenticationFilter clientAuthenticationFilter = new OAuth2ClientAuthenticationFilter(
|
||||
authenticationManager, this.requestMatcher);
|
||||
if (this.authenticationConverter != null) {
|
||||
clientAuthenticationFilter.setAuthenticationConverter(this.authenticationConverter);
|
||||
}
|
||||
if (this.authenticationSuccessHandler != null) {
|
||||
clientAuthenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
|
||||
}
|
||||
if (this.errorResponseHandler != null) {
|
||||
clientAuthenticationFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
|
||||
}
|
||||
builder.addFilterAfter(postProcess(clientAuthenticationFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
RequestMatcher getRequestMatcher() {
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
RegisteredClientRepository registeredClientRepository = OAuth2ConfigurerUtils.getRegisteredClientRepository(builder);
|
||||
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(builder);
|
||||
|
||||
JwtClientAssertionAuthenticationProvider jwtClientAssertionAuthenticationProvider =
|
||||
new JwtClientAssertionAuthenticationProvider(registeredClientRepository, authorizationService);
|
||||
authenticationProviders.add(jwtClientAssertionAuthenticationProvider);
|
||||
|
||||
ClientSecretAuthenticationProvider clientSecretAuthenticationProvider =
|
||||
new ClientSecretAuthenticationProvider(registeredClientRepository, authorizationService);
|
||||
PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(builder, PasswordEncoder.class);
|
||||
if (passwordEncoder != null) {
|
||||
clientSecretAuthenticationProvider.setPasswordEncoder(passwordEncoder);
|
||||
}
|
||||
authenticationProviders.add(clientSecretAuthenticationProvider);
|
||||
|
||||
PublicClientAuthenticationProvider publicClientAuthenticationProvider =
|
||||
new PublicClientAuthenticationProvider(registeredClientRepository, authorizationService);
|
||||
authenticationProviders.add(publicClientAuthenticationProvider);
|
||||
|
||||
return authenticationProviders;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Utility methods for the OAuth 2.0 Configurers.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
*/
|
||||
final class OAuth2ConfigurerUtils {
|
||||
|
||||
private OAuth2ConfigurerUtils() {
|
||||
}
|
||||
|
||||
static <B extends HttpSecurityBuilder<B>> RegisteredClientRepository getRegisteredClientRepository(B builder) {
|
||||
RegisteredClientRepository registeredClientRepository = builder.getSharedObject(RegisteredClientRepository.class);
|
||||
if (registeredClientRepository == null) {
|
||||
registeredClientRepository = getBean(builder, RegisteredClientRepository.class);
|
||||
builder.setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
|
||||
}
|
||||
return registeredClientRepository;
|
||||
}
|
||||
|
||||
static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService getAuthorizationService(B builder) {
|
||||
OAuth2AuthorizationService authorizationService = builder.getSharedObject(OAuth2AuthorizationService.class);
|
||||
if (authorizationService == null) {
|
||||
authorizationService = getOptionalBean(builder, OAuth2AuthorizationService.class);
|
||||
if (authorizationService == null) {
|
||||
authorizationService = new InMemoryOAuth2AuthorizationService();
|
||||
}
|
||||
builder.setSharedObject(OAuth2AuthorizationService.class, authorizationService);
|
||||
}
|
||||
return authorizationService;
|
||||
}
|
||||
|
||||
static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationConsentService getAuthorizationConsentService(B builder) {
|
||||
OAuth2AuthorizationConsentService authorizationConsentService = builder.getSharedObject(OAuth2AuthorizationConsentService.class);
|
||||
if (authorizationConsentService == null) {
|
||||
authorizationConsentService = getOptionalBean(builder, OAuth2AuthorizationConsentService.class);
|
||||
if (authorizationConsentService == null) {
|
||||
authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
|
||||
}
|
||||
builder.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
|
||||
}
|
||||
return authorizationConsentService;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <B extends HttpSecurityBuilder<B>> OAuth2TokenGenerator<? extends OAuth2Token> getTokenGenerator(B builder) {
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = builder.getSharedObject(OAuth2TokenGenerator.class);
|
||||
if (tokenGenerator == null) {
|
||||
tokenGenerator = getOptionalBean(builder, OAuth2TokenGenerator.class);
|
||||
if (tokenGenerator == null) {
|
||||
JwtGenerator jwtGenerator = getJwtGenerator(builder);
|
||||
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
|
||||
OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer = getAccessTokenCustomizer(builder);
|
||||
if (accessTokenCustomizer != null) {
|
||||
accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer);
|
||||
}
|
||||
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
|
||||
if (jwtGenerator != null) {
|
||||
tokenGenerator = new DelegatingOAuth2TokenGenerator(
|
||||
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
|
||||
} else {
|
||||
tokenGenerator = new DelegatingOAuth2TokenGenerator(
|
||||
accessTokenGenerator, refreshTokenGenerator);
|
||||
}
|
||||
}
|
||||
builder.setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
|
||||
}
|
||||
return tokenGenerator;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> JwtGenerator getJwtGenerator(B builder) {
|
||||
JwtGenerator jwtGenerator = builder.getSharedObject(JwtGenerator.class);
|
||||
if (jwtGenerator == null) {
|
||||
JwtEncoder jwtEncoder = getJwtEncoder(builder);
|
||||
if (jwtEncoder != null) {
|
||||
jwtGenerator = new JwtGenerator(jwtEncoder);
|
||||
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(builder);
|
||||
if (jwtCustomizer != null) {
|
||||
jwtGenerator.setJwtCustomizer(jwtCustomizer);
|
||||
}
|
||||
builder.setSharedObject(JwtGenerator.class, jwtGenerator);
|
||||
}
|
||||
}
|
||||
return jwtGenerator;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
|
||||
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
|
||||
if (jwtEncoder == null) {
|
||||
jwtEncoder = getOptionalBean(builder, JwtEncoder.class);
|
||||
if (jwtEncoder == null) {
|
||||
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
|
||||
if (jwkSource != null) {
|
||||
jwtEncoder = new NimbusJwsEncoder(jwkSource);
|
||||
}
|
||||
}
|
||||
if (jwtEncoder != null) {
|
||||
builder.setSharedObject(JwtEncoder.class, jwtEncoder);
|
||||
}
|
||||
}
|
||||
return jwtEncoder;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <B extends HttpSecurityBuilder<B>> JWKSource<SecurityContext> getJwkSource(B builder) {
|
||||
JWKSource<SecurityContext> jwkSource = builder.getSharedObject(JWKSource.class);
|
||||
if (jwkSource == null) {
|
||||
ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class);
|
||||
jwkSource = getOptionalBean(builder, type);
|
||||
if (jwkSource != null) {
|
||||
builder.setSharedObject(JWKSource.class, jwkSource);
|
||||
}
|
||||
}
|
||||
return jwkSource;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(B builder) {
|
||||
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class);
|
||||
return getOptionalBean(builder, type);
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> OAuth2TokenCustomizer<OAuth2TokenClaimsContext> getAccessTokenCustomizer(B builder) {
|
||||
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, OAuth2TokenClaimsContext.class);
|
||||
return getOptionalBean(builder, type);
|
||||
}
|
||||
|
||||
static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettings(B builder) {
|
||||
ProviderSettings providerSettings = builder.getSharedObject(ProviderSettings.class);
|
||||
if (providerSettings == null) {
|
||||
providerSettings = getBean(builder, ProviderSettings.class);
|
||||
builder.setSharedObject(ProviderSettings.class, providerSettings);
|
||||
}
|
||||
return providerSettings;
|
||||
}
|
||||
|
||||
static <B extends HttpSecurityBuilder<B>, T> T getBean(B builder, Class<T> type) {
|
||||
return builder.getSharedObject(ApplicationContext.class).getBean(type);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <B extends HttpSecurityBuilder<B>, T> T getBean(B builder, ResolvableType type) {
|
||||
ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
|
||||
String[] names = context.getBeanNamesForType(type);
|
||||
if (names.length == 1) {
|
||||
return (T) context.getBean(names[0]);
|
||||
}
|
||||
if (names.length > 1) {
|
||||
throw new NoUniqueBeanDefinitionException(type, names);
|
||||
}
|
||||
throw new NoSuchBeanDefinitionException(type);
|
||||
}
|
||||
|
||||
static <B extends HttpSecurityBuilder<B>, T> T getOptionalBean(B builder, Class<T> type) {
|
||||
Map<String, T> beansMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
|
||||
builder.getSharedObject(ApplicationContext.class), type);
|
||||
if (beansMap.size() > 1) {
|
||||
throw new NoUniqueBeanDefinitionException(type, beansMap.size(),
|
||||
"Expected single matching bean of type '" + type.getName() + "' but found " +
|
||||
beansMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(beansMap.keySet()));
|
||||
}
|
||||
return (!beansMap.isEmpty() ? beansMap.values().iterator().next() : null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <B extends HttpSecurityBuilder<B>, T> T getOptionalBean(B builder, ResolvableType type) {
|
||||
ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
|
||||
String[] names = context.getBeanNamesForType(type);
|
||||
if (names.length > 1) {
|
||||
throw new NoUniqueBeanDefinitionException(type, names);
|
||||
}
|
||||
return names.length == 1 ? (T) context.getBean(names[0]) : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Configurer for the OAuth 2.0 Token Endpoint.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationServerConfigurer#tokenEndpoint
|
||||
* @see OAuth2TokenEndpointFilter
|
||||
*/
|
||||
public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private AuthenticationConverter accessTokenRequestConverter;
|
||||
private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
|
||||
private AuthenticationSuccessHandler accessTokenResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OAuth2TokenEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2AuthorizationGrantAuthenticationToken} used for authenticating the authorization grant.
|
||||
*
|
||||
* @param accessTokenRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
|
||||
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenEndpointConfigurer accessTokenRequestConverter(AuthenticationConverter accessTokenRequestConverter) {
|
||||
this.accessTokenRequestConverter = accessTokenRequestConverter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2AuthorizationGrantAuthenticationToken}.
|
||||
*
|
||||
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2AuthorizationGrantAuthenticationToken}
|
||||
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
|
||||
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
|
||||
this.authenticationProviders.add(authenticationProvider);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AccessTokenAuthenticationToken}
|
||||
* and returning the {@link OAuth2AccessTokenResponse Access Token Response}.
|
||||
*
|
||||
* @param accessTokenResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AccessTokenAuthenticationToken}
|
||||
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenEndpointConfigurer accessTokenResponseHandler(AuthenticationSuccessHandler accessTokenResponseHandler) {
|
||||
this.accessTokenResponseHandler = accessTokenResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* and returning the {@link OAuth2Error Error Response}.
|
||||
*
|
||||
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
|
||||
this.errorResponseHandler = errorResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void init(B builder) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.getTokenEndpoint(), HttpMethod.POST.name());
|
||||
|
||||
List<AuthenticationProvider> authenticationProviders =
|
||||
!this.authenticationProviders.isEmpty() ?
|
||||
this.authenticationProviders :
|
||||
createDefaultAuthenticationProviders(builder);
|
||||
authenticationProviders.forEach(authenticationProvider ->
|
||||
builder.authenticationProvider(postProcess(authenticationProvider)));
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
|
||||
OAuth2TokenEndpointFilter tokenEndpointFilter =
|
||||
new OAuth2TokenEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.getTokenEndpoint());
|
||||
if (this.accessTokenRequestConverter != null) {
|
||||
tokenEndpointFilter.setAuthenticationConverter(this.accessTokenRequestConverter);
|
||||
}
|
||||
if (this.accessTokenResponseHandler != null) {
|
||||
tokenEndpointFilter.setAuthenticationSuccessHandler(this.accessTokenResponseHandler);
|
||||
}
|
||||
if (this.errorResponseHandler != null) {
|
||||
tokenEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
|
||||
}
|
||||
builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
RequestMatcher getRequestMatcher() {
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(builder);
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = OAuth2ConfigurerUtils.getTokenGenerator(builder);
|
||||
|
||||
OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
|
||||
new OAuth2AuthorizationCodeAuthenticationProvider(authorizationService, tokenGenerator);
|
||||
authenticationProviders.add(authorizationCodeAuthenticationProvider);
|
||||
|
||||
OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
|
||||
new OAuth2RefreshTokenAuthenticationProvider(authorizationService, tokenGenerator);
|
||||
authenticationProviders.add(refreshTokenAuthenticationProvider);
|
||||
|
||||
OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider =
|
||||
new OAuth2ClientCredentialsAuthenticationProvider(authorizationService, tokenGenerator);
|
||||
authenticationProviders.add(clientCredentialsAuthenticationProvider);
|
||||
|
||||
return authenticationProviders;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Configurer for the OAuth 2.0 Token Introspection Endpoint.
|
||||
*
|
||||
* @author Gaurav Tiwari
|
||||
* @since 0.2.3
|
||||
* @see OAuth2AuthorizationServerConfigurer#tokenIntrospectionEndpoint(Customizer)
|
||||
* @see OAuth2TokenIntrospectionEndpointFilter
|
||||
*/
|
||||
public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private AuthenticationConverter introspectionRequestConverter;
|
||||
private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
|
||||
private AuthenticationSuccessHandler introspectionResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OAuth2TokenIntrospectionEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2TokenIntrospectionAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @param introspectionRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest}
|
||||
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenIntrospectionEndpointConfigurer introspectionRequestConverter(AuthenticationConverter introspectionRequestConverter) {
|
||||
this.introspectionRequestConverter = introspectionRequestConverter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenIntrospectionAuthenticationToken}.
|
||||
*
|
||||
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenIntrospectionAuthenticationToken}
|
||||
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenIntrospectionEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
|
||||
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
|
||||
this.authenticationProviders.add(authenticationProvider);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}.
|
||||
*
|
||||
* @param introspectionResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}
|
||||
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenIntrospectionEndpointConfigurer introspectionResponseHandler(AuthenticationSuccessHandler introspectionResponseHandler) {
|
||||
this.introspectionResponseHandler = introspectionResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* and returning the {@link OAuth2Error Error Response}.
|
||||
*
|
||||
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenIntrospectionEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
|
||||
this.errorResponseHandler = errorResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void init(B builder) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.getTokenIntrospectionEndpoint(), HttpMethod.POST.name());
|
||||
|
||||
List<AuthenticationProvider> authenticationProviders =
|
||||
!this.authenticationProviders.isEmpty() ?
|
||||
this.authenticationProviders :
|
||||
createDefaultAuthenticationProviders(builder);
|
||||
authenticationProviders.forEach(authenticationProvider ->
|
||||
builder.authenticationProvider(postProcess(authenticationProvider)));
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
|
||||
OAuth2TokenIntrospectionEndpointFilter introspectionEndpointFilter =
|
||||
new OAuth2TokenIntrospectionEndpointFilter(
|
||||
authenticationManager, providerSettings.getTokenIntrospectionEndpoint());
|
||||
if (this.introspectionRequestConverter != null) {
|
||||
introspectionEndpointFilter.setAuthenticationConverter(this.introspectionRequestConverter);
|
||||
}
|
||||
if (this.introspectionResponseHandler != null) {
|
||||
introspectionEndpointFilter.setAuthenticationSuccessHandler(this.introspectionResponseHandler);
|
||||
}
|
||||
if (this.errorResponseHandler != null) {
|
||||
introspectionEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
|
||||
}
|
||||
builder.addFilterAfter(postProcess(introspectionEndpointFilter), FilterSecurityInterceptor.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestMatcher getRequestMatcher() {
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
|
||||
new OAuth2TokenIntrospectionAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(builder));
|
||||
authenticationProviders.add(tokenIntrospectionAuthenticationProvider);
|
||||
|
||||
return authenticationProviders;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Configurer for the OAuth 2.0 Token Revocation Endpoint.
|
||||
*
|
||||
* @author Arfat Chaus
|
||||
* @since 0.2.2
|
||||
* @see OAuth2AuthorizationServerConfigurer#tokenRevocationEndpoint
|
||||
* @see OAuth2TokenRevocationEndpointFilter
|
||||
*/
|
||||
public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private AuthenticationConverter revocationRequestConverter;
|
||||
private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
|
||||
private AuthenticationSuccessHandler revocationResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OAuth2TokenRevocationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the client.
|
||||
*
|
||||
* @param revocationRequestConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
|
||||
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenRevocationEndpointConfigurer revocationRequestConverter(AuthenticationConverter revocationRequestConverter) {
|
||||
this.revocationRequestConverter = revocationRequestConverter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenRevocationAuthenticationToken}.
|
||||
*
|
||||
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenRevocationAuthenticationToken}
|
||||
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenRevocationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
|
||||
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
|
||||
this.authenticationProviders.add(authenticationProvider);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenRevocationAuthenticationToken}.
|
||||
*
|
||||
* @param revocationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenRevocationAuthenticationToken}
|
||||
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenRevocationEndpointConfigurer revocationResponseHandler(AuthenticationSuccessHandler revocationResponseHandler) {
|
||||
this.revocationResponseHandler = revocationResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* and returning the {@link OAuth2Error Error Response}.
|
||||
*
|
||||
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenRevocationEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
|
||||
this.errorResponseHandler = errorResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void init(B builder) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name());
|
||||
|
||||
List<AuthenticationProvider> authenticationProviders =
|
||||
!this.authenticationProviders.isEmpty() ?
|
||||
this.authenticationProviders :
|
||||
createDefaultAuthenticationProviders(builder);
|
||||
authenticationProviders.forEach(authenticationProvider ->
|
||||
builder.authenticationProvider(postProcess(authenticationProvider)));
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
|
||||
OAuth2TokenRevocationEndpointFilter revocationEndpointFilter =
|
||||
new OAuth2TokenRevocationEndpointFilter(
|
||||
authenticationManager, providerSettings.getTokenRevocationEndpoint());
|
||||
if (this.revocationRequestConverter != null) {
|
||||
revocationEndpointFilter.setAuthenticationConverter(this.revocationRequestConverter);
|
||||
}
|
||||
if (this.revocationResponseHandler != null) {
|
||||
revocationEndpointFilter.setAuthenticationSuccessHandler(this.revocationResponseHandler);
|
||||
}
|
||||
if (this.errorResponseHandler != null) {
|
||||
revocationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
|
||||
}
|
||||
builder.addFilterAfter(postProcess(revocationEndpointFilter), FilterSecurityInterceptor.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
RequestMatcher getRequestMatcher() {
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
|
||||
new OAuth2TokenRevocationAuthenticationProvider(OAuth2ConfigurerUtils.getAuthorizationService(builder));
|
||||
authenticationProviders.add(tokenRevocationAuthenticationProvider);
|
||||
|
||||
return authenticationProviders;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
/**
|
||||
* Configurer for OpenID Connect Dynamic Client Registration 1.0 Endpoint.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.0
|
||||
* @see OidcConfigurer#clientRegistrationEndpoint
|
||||
* @see OidcClientRegistrationEndpointFilter
|
||||
*/
|
||||
public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OidcClientRegistrationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void init(B builder) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
this.requestMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(providerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(providerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.GET.name())
|
||||
);
|
||||
|
||||
OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider =
|
||||
new OidcClientRegistrationAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(builder),
|
||||
OAuth2ConfigurerUtils.getTokenGenerator(builder));
|
||||
builder.authenticationProvider(postProcess(oidcClientRegistrationAuthenticationProvider));
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
|
||||
OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter =
|
||||
new OidcClientRegistrationEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.getOidcClientRegistrationEndpoint());
|
||||
builder.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), FilterSecurityInterceptor.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
RequestMatcher getRequestMatcher() {
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
/**
|
||||
* Configurer for OpenID Connect 1.0 support.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.0
|
||||
* @see OAuth2AuthorizationServerConfigurer#oidc
|
||||
* @see OidcClientRegistrationEndpointConfigurer
|
||||
* @see OidcUserInfoEndpointConfigurer
|
||||
* @see OidcProviderConfigurationEndpointFilter
|
||||
*/
|
||||
public final class OidcConfigurer extends AbstractOAuth2Configurer {
|
||||
private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
|
||||
private RequestMatcher requestMatcher;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OidcConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
addConfigurer(OidcUserInfoEndpointConfigurer.class, new OidcUserInfoEndpointConfigurer(objectPostProcessor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OpenID Connect Dynamic Client Registration 1.0 Endpoint.
|
||||
*
|
||||
* @param clientRegistrationEndpointCustomizer the {@link Customizer} providing access to the {@link OidcClientRegistrationEndpointConfigurer}
|
||||
* @return the {@link OidcConfigurer} for further configuration
|
||||
*/
|
||||
public OidcConfigurer clientRegistrationEndpoint(Customizer<OidcClientRegistrationEndpointConfigurer> clientRegistrationEndpointCustomizer) {
|
||||
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
|
||||
getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
|
||||
if (clientRegistrationEndpointConfigurer == null) {
|
||||
addConfigurer(OidcClientRegistrationEndpointConfigurer.class,
|
||||
new OidcClientRegistrationEndpointConfigurer(getObjectPostProcessor()));
|
||||
clientRegistrationEndpointConfigurer = getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
|
||||
}
|
||||
clientRegistrationEndpointCustomizer.customize(clientRegistrationEndpointConfigurer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OpenID Connect 1.0 UserInfo Endpoint.
|
||||
*
|
||||
* @param userInfoEndpointCustomizer the {@link Customizer} providing access to the {@link OidcUserInfoEndpointConfigurer}
|
||||
* @return the {@link OidcConfigurer} for further configuration
|
||||
*/
|
||||
public OidcConfigurer userInfoEndpoint(Customizer<OidcUserInfoEndpointConfigurer> userInfoEndpointCustomizer) {
|
||||
userInfoEndpointCustomizer.customize(getConfigurer(OidcUserInfoEndpointConfigurer.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void init(B builder) {
|
||||
OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer =
|
||||
getConfigurer(OidcUserInfoEndpointConfigurer.class);
|
||||
userInfoEndpointConfigurer.init(builder);
|
||||
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
|
||||
getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
|
||||
if (clientRegistrationEndpointConfigurer != null) {
|
||||
clientRegistrationEndpointConfigurer.init(builder);
|
||||
}
|
||||
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
requestMatchers.add(new AntPathRequestMatcher(
|
||||
"/.well-known/openid-configuration", HttpMethod.GET.name()));
|
||||
requestMatchers.add(userInfoEndpointConfigurer.getRequestMatcher());
|
||||
if (clientRegistrationEndpointConfigurer != null) {
|
||||
requestMatchers.add(clientRegistrationEndpointConfigurer.getRequestMatcher());
|
||||
}
|
||||
this.requestMatcher = new OrRequestMatcher(requestMatchers);
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
|
||||
OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer =
|
||||
getConfigurer(OidcUserInfoEndpointConfigurer.class);
|
||||
userInfoEndpointConfigurer.configure(builder);
|
||||
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
|
||||
getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
|
||||
if (clientRegistrationEndpointConfigurer != null) {
|
||||
clientRegistrationEndpointConfigurer.configure(builder);
|
||||
}
|
||||
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
|
||||
new OidcProviderConfigurationEndpointFilter(providerSettings);
|
||||
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
RequestMatcher getRequestMatcher() {
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> T getConfigurer(Class<T> type) {
|
||||
return (T) this.configurers.get(type);
|
||||
}
|
||||
|
||||
private <T extends AbstractOAuth2Configurer> void addConfigurer(Class<T> configurerType, T configurer) {
|
||||
this.configurers.put(configurerType, configurer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcUserInfoEndpointFilter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
/**
|
||||
* Configurer for OpenID Connect 1.0 UserInfo Endpoint.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.2.1
|
||||
* @see OidcConfigurer#userInfoEndpoint
|
||||
* @see OidcUserInfoEndpointFilter
|
||||
*/
|
||||
public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OidcUserInfoEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Function} used to extract claims from {@link OidcUserInfoAuthenticationContext}
|
||||
* to an instance of {@link OidcUserInfo} for the UserInfo response.
|
||||
*
|
||||
* <p>
|
||||
* The {@link OidcUserInfoAuthenticationContext} gives the mapper access to the {@link OidcUserInfoAuthenticationToken},
|
||||
* as well as, the following context attributes:
|
||||
* <ul>
|
||||
* <li>{@link OidcUserInfoAuthenticationContext#getAccessToken()} containing the bearer token used to make the request.</li>
|
||||
* <li>{@link OidcUserInfoAuthenticationContext#getAuthorization()} containing the {@link OidcIdToken} and
|
||||
* {@link OAuth2AccessToken} associated with the bearer token used to make the request.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param userInfoMapper the {@link Function} used to extract claims from {@link OidcUserInfoAuthenticationContext} to an instance of {@link OidcUserInfo}
|
||||
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OidcUserInfoEndpointConfigurer userInfoMapper(Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper) {
|
||||
this.userInfoMapper = userInfoMapper;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void init(B builder) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
String userInfoEndpointUri = providerSettings.getOidcUserInfoEndpoint();
|
||||
this.requestMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.GET.name()),
|
||||
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.POST.name()));
|
||||
|
||||
OidcUserInfoAuthenticationProvider oidcUserInfoAuthenticationProvider =
|
||||
new OidcUserInfoAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(builder));
|
||||
if (this.userInfoMapper != null) {
|
||||
oidcUserInfoAuthenticationProvider.setUserInfoMapper(this.userInfoMapper);
|
||||
}
|
||||
builder.authenticationProvider(postProcess(oidcUserInfoAuthenticationProvider));
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
|
||||
OidcUserInfoEndpointFilter oidcUserInfoEndpointFilter =
|
||||
new OidcUserInfoEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.getOidcUserInfoEndpoint());
|
||||
builder.addFilterAfter(postProcess(oidcUserInfoEndpointFilter), FilterSecurityInterceptor.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
RequestMatcher getRequestMatcher() {
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -73,7 +73,7 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
|
||||
@SuppressWarnings("unchecked")
|
||||
protected final B getThis() {
|
||||
return (B) this; // avoid unchecked casts in subclasses by using "getThis()" instead of "(B) this"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@code issuer} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.
|
||||
@@ -391,7 +391,7 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
|
||||
valuesConsumer.accept(values);
|
||||
}
|
||||
|
||||
private static void validateURL(Object url, String errorMessage) {
|
||||
protected static void validateURL(Object url, String errorMessage) {
|
||||
if (URL.class.isAssignableFrom(url.getClass())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -13,9 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
package org.springframework.security.oauth2.core;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* 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.oauth2.core;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.3
|
||||
* @see <a target="_blank" href="https://github.com/spring-projects/spring-security/pull/9146">Issue gh-9146</a>
|
||||
*/
|
||||
public class OAuth2RefreshToken2 extends OAuth2RefreshToken {
|
||||
private final Instant expiresAt;
|
||||
|
||||
public OAuth2RefreshToken2(String tokenValue, Instant issuedAt, Instant expiresAt) {
|
||||
super(tokenValue, issuedAt);
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getExpiresAt() {
|
||||
return this.expiresAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Standard data formats for OAuth 2.0 Tokens.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public final class OAuth2TokenFormat implements Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
|
||||
/**
|
||||
* Self-contained tokens use a protected, time-limited data structure that contains token metadata
|
||||
* and claims of the user and/or client. JSON Web Token (JWT) is a widely used format.
|
||||
*/
|
||||
public static final OAuth2TokenFormat SELF_CONTAINED = new OAuth2TokenFormat("self-contained");
|
||||
|
||||
/**
|
||||
* Reference (opaque) tokens are unique identifiers that serve as a reference
|
||||
* to the token metadata and claims of the user and/or client, stored at the provider.
|
||||
*/
|
||||
public static final OAuth2TokenFormat REFERENCE = new OAuth2TokenFormat("reference");
|
||||
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2TokenFormat} using the provided value.
|
||||
*
|
||||
* @param value the value of the token format
|
||||
*/
|
||||
public OAuth2TokenFormat(String value) {
|
||||
Assert.hasText(value, "value cannot be empty");
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the token format.
|
||||
*
|
||||
* @return the value of the token format
|
||||
*/
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || this.getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
OAuth2TokenFormat that = (OAuth2TokenFormat) obj;
|
||||
return getValue().equals(that.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getValue().hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,7 +41,9 @@ import java.util.List;
|
||||
* @see OAuth2TokenIntrospectionClaimNames
|
||||
* @see <a target="_blank" href=
|
||||
* "https://tools.ietf.org/html/rfc7662#section-2.2">Introspection Response</a>
|
||||
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/597">gh-597</a>
|
||||
*/
|
||||
@Deprecated
|
||||
public interface OAuth2TokenIntrospectionClaimAccessor extends ClaimAccessor {
|
||||
|
||||
/**
|
||||
@@ -56,7 +58,7 @@ public interface OAuth2TokenIntrospectionClaimAccessor extends ClaimAccessor {
|
||||
* Returns the scopes {@code (scope)} associated with the token
|
||||
* @return the scopes associated with the token
|
||||
*/
|
||||
default List<String> getScope() {
|
||||
default List<String> getScopes() {
|
||||
return getClaimAsStringList(OAuth2TokenIntrospectionClaimNames.SCOPE);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ package org.springframework.security.oauth2.core;
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.2
|
||||
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/597">gh-597</a>
|
||||
*/
|
||||
@Deprecated
|
||||
public interface OAuth2TokenIntrospectionClaimNames {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,8 +23,8 @@ package org.springframework.security.oauth2.core;
|
||||
*/
|
||||
public final class Version {
|
||||
private static final int MAJOR = 0;
|
||||
private static final int MINOR = 1;
|
||||
private static final int PATCH = 1;
|
||||
private static final int MINOR = 2;
|
||||
private static final int PATCH = 3;
|
||||
|
||||
/**
|
||||
* Global Serialization value for Spring Security Authorization Server classes.
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.context.Context;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A context that holds an {@link Authentication} and (optionally) additional information.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.0
|
||||
* @see Context
|
||||
*/
|
||||
public class OAuth2AuthenticationContext implements Context {
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthenticationContext} using the provided parameters.
|
||||
*
|
||||
* @param authentication the {@code Authentication}
|
||||
* @param context a {@code Map} of additional context information
|
||||
*/
|
||||
public OAuth2AuthenticationContext(Authentication authentication, @Nullable Map<Object, Object> context) {
|
||||
Assert.notNull(authentication, "authentication cannot be null");
|
||||
Map<Object, Object> ctx = new HashMap<>();
|
||||
if (!CollectionUtils.isEmpty(context)) {
|
||||
ctx.putAll(context);
|
||||
}
|
||||
ctx.put(Authentication.class, authentication);
|
||||
this.context = Collections.unmodifiableMap(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthenticationContext} using the provided parameters.
|
||||
*
|
||||
* @param context a {@code Map} of context information, must contain the {@code Authentication}
|
||||
* @since 0.2.1
|
||||
*/
|
||||
public OAuth2AuthenticationContext(Map<Object, Object> context) {
|
||||
Assert.notEmpty(context, "context cannot be empty");
|
||||
Assert.notNull(context.get(Authentication.class), "authentication cannot be null");
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Authentication} associated to the context.
|
||||
*
|
||||
* @param <T> the type of the {@code Authentication}
|
||||
* @return the {@link Authentication}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Authentication> T getAuthentication() {
|
||||
return (T) get(Authentication.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for subclasses of {@link OAuth2AuthenticationContext}.
|
||||
*
|
||||
* @param <T> the type of the authentication context
|
||||
* @param <B> the type of the builder
|
||||
* @since 0.2.1
|
||||
*/
|
||||
protected static abstract class AbstractBuilder<T extends OAuth2AuthenticationContext, B extends AbstractBuilder<T, B>> {
|
||||
private final Map<Object, Object> context = new HashMap<>();
|
||||
|
||||
protected AbstractBuilder(Authentication authentication) {
|
||||
Assert.notNull(authentication, "authentication cannot be null");
|
||||
put(Authentication.class, authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates an attribute.
|
||||
*
|
||||
* @param key the key for the attribute
|
||||
* @param value the value of the attribute
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B put(Object key, Object value) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
getContext().put(key, value);
|
||||
return getThis();
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the attributes {@code Map}
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param contextConsumer a {@link Consumer} of the attributes {@code Map}
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B context(Consumer<Map<Object, Object>> contextConsumer) {
|
||||
contextConsumer.accept(getContext());
|
||||
return getThis();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <V> V get(Object key) {
|
||||
return (V) getContext().get(key);
|
||||
}
|
||||
|
||||
protected Map<Object, Object> getContext() {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected final B getThis() {
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OAuth2AuthenticationContext}.
|
||||
*
|
||||
* @return the {@link OAuth2AuthenticationContext}
|
||||
*/
|
||||
public abstract T build();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.authentication;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for validating the attribute(s)
|
||||
* of the {@link Authentication} associated to the {@link OAuth2AuthenticationContext}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.0
|
||||
* @see OAuth2AuthenticationContext
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface OAuth2AuthenticationValidator {
|
||||
|
||||
/**
|
||||
* Validate the attribute(s) of the {@link Authentication}.
|
||||
*
|
||||
* @param authenticationContext the authentication context
|
||||
* @throws OAuth2AuthenticationException if the attribute(s) of the {@code Authentication} is invalid
|
||||
*/
|
||||
void validate(OAuth2AuthenticationContext authenticationContext) throws OAuth2AuthenticationException;
|
||||
|
||||
}
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.context;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -28,9 +26,23 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public interface Context {
|
||||
|
||||
/**
|
||||
* Returns the value of the attribute associated to the key.
|
||||
*
|
||||
* @param key the key for the attribute
|
||||
* @param <V> the type of the value for the attribute
|
||||
* @return the value of the attribute associated to the key, or {@code null} if not available
|
||||
*/
|
||||
@Nullable
|
||||
<V> V get(Object key);
|
||||
|
||||
/**
|
||||
* Returns the value of the attribute associated to the key.
|
||||
*
|
||||
* @param key the key for the attribute
|
||||
* @param <V> the type of the value for the attribute
|
||||
* @return the value of the attribute associated to the key, or {@code null} if not available or not of the specified type
|
||||
*/
|
||||
@Nullable
|
||||
default <V> V get(Class<V> key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
@@ -38,10 +50,12 @@ public interface Context {
|
||||
return key.isInstance(value) ? value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if an attribute associated to the key exists, {@code false} otherwise.
|
||||
*
|
||||
* @param key the key for the attribute
|
||||
* @return {@code true} if an attribute associated to the key exists, {@code false} otherwise
|
||||
*/
|
||||
boolean hasKey(Object key);
|
||||
|
||||
static Context of(Map<Object, Object> context) {
|
||||
return new DefaultContext(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -184,8 +184,8 @@ public class OAuth2TokenIntrospectionHttpMessageConverter extends AbstractHttpMe
|
||||
@Override
|
||||
public Map<String, Object> convert(OAuth2TokenIntrospection source) {
|
||||
Map<String, Object> responseClaims = new LinkedHashMap<>(source.getClaims());
|
||||
if (!CollectionUtils.isEmpty(source.getScope())) {
|
||||
responseClaims.put(OAuth2TokenIntrospectionClaimNames.SCOPE, StringUtils.collectionToDelimitedString(source.getScope(), " "));
|
||||
if (!CollectionUtils.isEmpty(source.getScopes())) {
|
||||
responseClaims.put(OAuth2TokenIntrospectionClaimNames.SCOPE, StringUtils.collectionToDelimitedString(source.getScopes(), " "));
|
||||
}
|
||||
if (source.getExpiresAt() != null) {
|
||||
responseClaims.put(OAuth2TokenIntrospectionClaimNames.EXP, source.getExpiresAt().getEpochSecond());
|
||||
|
||||
@@ -15,11 +15,15 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.oidc;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
/**
|
||||
* A {@link ClaimAccessor} for the "claims" that are contained
|
||||
@@ -98,6 +102,18 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
|
||||
return getClaimAsString(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
|
||||
* the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
|
||||
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods {@code (token_endpoint_auth_signing_alg)}.
|
||||
*
|
||||
* @return the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate the Client at the Token Endpoint
|
||||
* @since 0.2.2
|
||||
*/
|
||||
default String getTokenEndpointAuthenticationSigningAlgorithm() {
|
||||
return getClaimAsString(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OAuth 2.0 {@code grant_type} values that the Client will restrict itself to using {@code (grant_types)}.
|
||||
*
|
||||
@@ -125,6 +141,16 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
|
||||
return getClaimAsStringList(OidcClientMetadataClaimNames.SCOPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}.
|
||||
*
|
||||
* @return the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
|
||||
* @since 0.2.2
|
||||
*/
|
||||
default URL getJwkSetUrl() {
|
||||
return getClaimAsURL(OidcClientMetadataClaimNames.JWKS_URI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link SignatureAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client {@code (id_token_signed_response_alg)}.
|
||||
*
|
||||
@@ -134,4 +160,24 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
|
||||
return getClaimAsString(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Registration Access Token that can be used at the Client Configuration Endpoint.
|
||||
*
|
||||
* @return the Registration Access Token that can be used at the Client Configuration Endpoint
|
||||
* @since 0.2.1
|
||||
*/
|
||||
default String getRegistrationAccessToken() {
|
||||
return getClaimAsString(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code URL} of the Client Configuration Endpoint where the Registration Access Token can be used.
|
||||
*
|
||||
* @return the {@code URL} of the Client Configuration Endpoint where the Registration Access Token can be used
|
||||
* @since 0.2.1
|
||||
*/
|
||||
default URL getRegistrationClientUrl() {
|
||||
return getClaimAsURL(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.oidc;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
/**
|
||||
* The names of the "claims" defined by OpenID Connect Dynamic Client Registration 1.0
|
||||
@@ -63,6 +65,14 @@ public interface OidcClientMetadataClaimNames {
|
||||
*/
|
||||
String TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method";
|
||||
|
||||
/**
|
||||
* {@code token_endpoint_auth_signing_alg} - the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
|
||||
* used to authenticate the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
|
||||
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods
|
||||
* @since 0.2.2
|
||||
*/
|
||||
String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "token_endpoint_auth_signing_alg";
|
||||
|
||||
/**
|
||||
* {@code grant_types} - the OAuth 2.0 {@code grant_type} values that the Client will restrict itself to using
|
||||
*/
|
||||
@@ -78,9 +88,27 @@ public interface OidcClientMetadataClaimNames {
|
||||
*/
|
||||
String SCOPE = "scope";
|
||||
|
||||
/**
|
||||
* {@code jwks_uri} - the {@code URL} for the Client's JSON Web Key Set
|
||||
* @since 0.2.2
|
||||
*/
|
||||
String JWKS_URI = "jwks_uri";
|
||||
|
||||
/**
|
||||
* {@code id_token_signed_response_alg} - the {@link JwsAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client
|
||||
*/
|
||||
String ID_TOKEN_SIGNED_RESPONSE_ALG = "id_token_signed_response_alg";
|
||||
|
||||
/**
|
||||
* {@code registration_access_token} - the Registration Access Token that can be used at the Client Configuration Endpoint
|
||||
* @since 0.2.1
|
||||
*/
|
||||
String REGISTRATION_ACCESS_TOKEN = "registration_access_token";
|
||||
|
||||
/**
|
||||
* {@code registration_client_uri} - the {@code URL} of the Client Configuration Endpoint where the Registration Access Token can be used
|
||||
* @since 0.2.1
|
||||
*/
|
||||
String REGISTRATION_CLIENT_URI = "registration_client_uri";
|
||||
|
||||
}
|
||||
|
||||
@@ -26,8 +26,11 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -174,6 +177,20 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
|
||||
return claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, tokenEndpointAuthenticationMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
|
||||
* the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
|
||||
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods, OPTIONAL.
|
||||
|
||||
* @param authenticationSigningAlgorithm the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
|
||||
* used to authenticate the Client at the Token Endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public Builder tokenEndpointAuthenticationSigningAlgorithm(String authenticationSigningAlgorithm) {
|
||||
return claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, authenticationSigningAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the OAuth 2.0 {@code grant_type} that the Client will restrict itself to using, OPTIONAL.
|
||||
*
|
||||
@@ -243,6 +260,17 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code URL} for the Client's JSON Web Key Set, OPTIONAL.
|
||||
*
|
||||
* @param jwkSetUrl the {@code URL} for the Client's JSON Web Key Set
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public Builder jwkSetUrl(String jwkSetUrl) {
|
||||
return claim(OidcClientMetadataClaimNames.JWKS_URI, jwkSetUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SignatureAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client, OPTIONAL.
|
||||
*
|
||||
@@ -253,6 +281,28 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
|
||||
return claim(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, idTokenSignedResponseAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Registration Access Token that can be used at the Client Configuration Endpoint, OPTIONAL.
|
||||
*
|
||||
* @param registrationAccessToken the Registration Access Token that can be used at the Client Configuration Endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 0.2.1
|
||||
*/
|
||||
public Builder registrationAccessToken(String registrationAccessToken) {
|
||||
return claim(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN, registrationAccessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code URL} of the Client Configuration Endpoint where the Registration Access Token can be used, OPTIONAL.
|
||||
*
|
||||
* @param registrationClientUrl the {@code URL} of the Client Configuration Endpoint where the Registration Access Token can be used
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 0.2.1
|
||||
*/
|
||||
public Builder registrationClientUrl(String registrationClientUrl) {
|
||||
return claim(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI, registrationClientUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the claim.
|
||||
*
|
||||
@@ -307,9 +357,6 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
|
||||
Assert.notNull(this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris cannot be null");
|
||||
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris must be of type List");
|
||||
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris cannot be empty");
|
||||
((List<?>) this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS)).forEach(
|
||||
url -> validateURL(url, "redirect_uri must be a valid URL")
|
||||
);
|
||||
if (this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES) != null) {
|
||||
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES), "grant_types must be of type List");
|
||||
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES), "grant_types cannot be empty");
|
||||
@@ -322,6 +369,9 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
|
||||
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.SCOPE), "scope must be of type List");
|
||||
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.SCOPE), "scope cannot be empty");
|
||||
}
|
||||
if (this.claims.get(OidcClientMetadataClaimNames.JWKS_URI) != null) {
|
||||
validateURL(this.claims.get(OidcClientMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -345,11 +395,14 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
|
||||
if (URL.class.isAssignableFrom(url.getClass())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new URI(url.toString()).toURL();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -118,6 +118,17 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@code userinfo_endpoint} in the resulting {@link OidcProviderConfiguration}, OPTIONAL.
|
||||
*
|
||||
* @param userInfoEndpoint the {@code URL} of the OpenID Connect 1.0 UserInfo Endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public Builder userInfoEndpoint(String userInfoEndpoint) {
|
||||
return claim(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, userInfoEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the claims and build the {@link OidcProviderConfiguration}.
|
||||
* <p>
|
||||
@@ -144,6 +155,9 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization
|
||||
Assert.notNull(getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be null");
|
||||
Assert.isInstanceOf(List.class, getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms must be of type List");
|
||||
Assert.notEmpty((List<?>) getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be empty");
|
||||
if (getClaims().get(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT) != null) {
|
||||
validateURL(getClaims().get(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT), "userInfoEndpoint must be a valid URL");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.security.oauth2.core.oidc;
|
||||
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
@@ -56,4 +57,14 @@ public interface OidcProviderMetadataClaimAccessor extends OAuth2AuthorizationSe
|
||||
return getClaimAsStringList(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code URL} of the OpenID Connect 1.0 UserInfo Endpoint {@code (userinfo_endpoint)}.
|
||||
*
|
||||
* @return the {@code URL} of the OpenID Connect 1.0 UserInfo Endpoint
|
||||
* @since 0.2.2
|
||||
*/
|
||||
default URL getUserInfoEndpoint() {
|
||||
return getClaimAsURL(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -39,4 +39,10 @@ public interface OidcProviderMetadataClaimNames extends OAuth2AuthorizationServe
|
||||
*/
|
||||
String ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = "id_token_signing_alg_values_supported";
|
||||
|
||||
/**
|
||||
* {@code userinfo_endpoint} - the {@code URL} of the OpenID Connect 1.0 UserInfo Endpoint
|
||||
* @since 0.2.2
|
||||
*/
|
||||
String USER_INFO_ENDPOINT = "userinfo_endpoint";
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.oidc.http.converter;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -130,6 +131,7 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess
|
||||
private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
|
||||
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
|
||||
private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class);
|
||||
private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
|
||||
private static final Converter<Object, ?> INSTANT_CONVERTER = getConverter(INSTANT_TYPE_DESCRIPTOR);
|
||||
private final ClaimTypeConverter claimTypeConverter;
|
||||
|
||||
@@ -137,6 +139,7 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess
|
||||
Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
|
||||
Converter<Object, ?> collectionStringConverter = getConverter(
|
||||
TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
|
||||
Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
|
||||
|
||||
Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
|
||||
claimConverters.put(OidcClientMetadataClaimNames.CLIENT_ID, stringConverter);
|
||||
@@ -146,9 +149,11 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess
|
||||
claimConverters.put(OidcClientMetadataClaimNames.CLIENT_NAME, stringConverter);
|
||||
claimConverters.put(OidcClientMetadataClaimNames.REDIRECT_URIS, collectionStringConverter);
|
||||
claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, stringConverter);
|
||||
claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, stringConverter);
|
||||
claimConverters.put(OidcClientMetadataClaimNames.GRANT_TYPES, collectionStringConverter);
|
||||
claimConverters.put(OidcClientMetadataClaimNames.RESPONSE_TYPES, collectionStringConverter);
|
||||
claimConverters.put(OidcClientMetadataClaimNames.SCOPE, MapOidcClientRegistrationConverter::convertScope);
|
||||
claimConverters.put(OidcClientMetadataClaimNames.JWKS_URI, urlConverter);
|
||||
claimConverters.put(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, stringConverter);
|
||||
this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -140,6 +140,7 @@ public class OidcProviderConfigurationHttpMessageConverter
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, urlConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.JWKS_URI, urlConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, urlConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, collectionStringConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, collectionStringConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, collectionStringConverter);
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.oidc.http.converter;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.AbstractHttpMessageConverter;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link HttpMessageConverter} for an {@link OidcUserInfo OpenID Connect UserInfo Response}.
|
||||
*
|
||||
* @author Ido Salomon
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.2.1
|
||||
* @see AbstractHttpMessageConverter
|
||||
* @see OidcUserInfo
|
||||
*/
|
||||
public class OidcUserInfoHttpMessageConverter extends AbstractHttpMessageConverter<OidcUserInfo> {
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
|
||||
private final GenericHttpMessageConverter<Object> jsonMessageConverter =
|
||||
HttpMessageConverters.getJsonMessageConverter();
|
||||
|
||||
private Converter<Map<String, Object>, OidcUserInfo> userInfoConverter = new MapOidcUserInfoConverter();
|
||||
private Converter<OidcUserInfo, Map<String, Object>> userInfoParametersConverter = OidcUserInfo::getClaims;
|
||||
|
||||
public OidcUserInfoHttpMessageConverter() {
|
||||
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return OidcUserInfo.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected OidcUserInfo readInternal(Class<? extends OidcUserInfo> clazz, HttpInputMessage inputMessage)
|
||||
throws HttpMessageNotReadableException {
|
||||
try {
|
||||
Map<String, Object> userInfoParameters =
|
||||
(Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
|
||||
return this.userInfoConverter.convert(userInfoParameters);
|
||||
} catch (Exception ex) {
|
||||
throw new HttpMessageNotReadableException(
|
||||
"An error occurred reading the UserInfo response: " + ex.getMessage(), ex, inputMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeInternal(OidcUserInfo oidcUserInfo, HttpOutputMessage outputMessage)
|
||||
throws HttpMessageNotWritableException {
|
||||
try {
|
||||
Map<String, Object> userInfoResponseParameters =
|
||||
this.userInfoParametersConverter.convert(oidcUserInfo);
|
||||
this.jsonMessageConverter.write(
|
||||
userInfoResponseParameters,
|
||||
STRING_OBJECT_MAP.getType(),
|
||||
MediaType.APPLICATION_JSON,
|
||||
outputMessage
|
||||
);
|
||||
} catch (Exception ex) {
|
||||
throw new HttpMessageNotWritableException(
|
||||
"An error occurred writing the UserInfo response: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Converter} used for converting the UserInfo parameters
|
||||
* to an {@link OidcUserInfo}.
|
||||
*
|
||||
* @param userInfoConverter the {@link Converter} used for converting to an {@link OidcUserInfo}
|
||||
*/
|
||||
public final void setUserInfoConverter(Converter<Map<String, Object>, OidcUserInfo> userInfoConverter) {
|
||||
Assert.notNull(userInfoConverter, "userInfoConverter cannot be null");
|
||||
this.userInfoConverter = userInfoConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Converter} used for converting the {@link OidcUserInfo} to a
|
||||
* {@code Map} representation of the UserInfo.
|
||||
*
|
||||
* @param userInfoParametersConverter the {@link Converter} used for converting to a
|
||||
* {@code Map} representation of the UserInfo
|
||||
*/
|
||||
public final void setUserInfoParametersConverter(
|
||||
Converter<OidcUserInfo, Map<String, Object>> userInfoParametersConverter) {
|
||||
Assert.notNull(userInfoParametersConverter, "userInfoParametersConverter cannot be null");
|
||||
this.userInfoParametersConverter = userInfoParametersConverter;
|
||||
}
|
||||
|
||||
private static final class MapOidcUserInfoConverter implements Converter<Map<String, Object>, OidcUserInfo> {
|
||||
private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance();
|
||||
private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
|
||||
private static final TypeDescriptor BOOLEAN_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Boolean.class);
|
||||
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
|
||||
private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class);
|
||||
private static final TypeDescriptor STRING_OBJECT_MAP_DESCRIPTOR = TypeDescriptor.map(Map.class, STRING_TYPE_DESCRIPTOR, OBJECT_TYPE_DESCRIPTOR);
|
||||
private final ClaimTypeConverter claimTypeConverter;
|
||||
|
||||
private MapOidcUserInfoConverter() {
|
||||
Converter<Object, ?> booleanConverter = getConverter(BOOLEAN_TYPE_DESCRIPTOR);
|
||||
Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
|
||||
Converter<Object, ?> instantConverter = getConverter(INSTANT_TYPE_DESCRIPTOR);
|
||||
Converter<Object, ?> mapConverter = getConverter(STRING_OBJECT_MAP_DESCRIPTOR);
|
||||
|
||||
Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
|
||||
claimConverters.put(StandardClaimNames.SUB, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.NAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.GIVEN_NAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.FAMILY_NAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.MIDDLE_NAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.NICKNAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PREFERRED_USERNAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PROFILE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PICTURE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.WEBSITE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.EMAIL, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
|
||||
claimConverters.put(StandardClaimNames.GENDER, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.BIRTHDATE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.ZONEINFO, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.LOCALE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PHONE_NUMBER, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
|
||||
claimConverters.put(StandardClaimNames.ADDRESS, mapConverter);
|
||||
claimConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
|
||||
|
||||
this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OidcUserInfo convert(Map<String, Object> source) {
|
||||
Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
|
||||
return new OidcUserInfo(parsedClaims);
|
||||
}
|
||||
|
||||
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
|
||||
return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,7 +24,7 @@ import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.JwaAlgorithm;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -36,10 +36,12 @@ import org.springframework.util.Assert;
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see Jwt
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-5">JWT JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-4">JWS JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-4">JWE JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7519#section-5">JWT JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7515#section-4">JWS JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7516#section-4">JWE JOSE Header</a>
|
||||
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
|
||||
*/
|
||||
@Deprecated
|
||||
public final class JoseHeader {
|
||||
private final Map<String, Object> headers;
|
||||
|
||||
@@ -48,12 +50,13 @@ public final class JoseHeader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JwsAlgorithm JWS algorithm} used to digitally sign the JWS.
|
||||
* Returns the {@link JwaAlgorithm JWA algorithm} used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @return the JWS algorithm
|
||||
* @return the {@link JwaAlgorithm}
|
||||
*/
|
||||
public JwsAlgorithm getJwsAlgorithm() {
|
||||
return getHeader(JoseHeaderNames.ALG);
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends JwaAlgorithm> T getAlgorithm() {
|
||||
return (T) getHeader(JoseHeaderNames.ALG);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,7 +65,7 @@ public final class JoseHeader {
|
||||
*
|
||||
* @return the JWK Set URL
|
||||
*/
|
||||
public URL getJwkSetUri() {
|
||||
public URL getJwkSetUrl() {
|
||||
return getHeader(JoseHeaderNames.JKU);
|
||||
}
|
||||
|
||||
@@ -91,13 +94,16 @@ public final class JoseHeader {
|
||||
*
|
||||
* @return the X.509 URL
|
||||
*/
|
||||
public URL getX509Uri() {
|
||||
public URL getX509Url() {
|
||||
return getHeader(JoseHeaderNames.X5U);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X.509 certificate chain that contains the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or
|
||||
* encrypt the JWE. The certificate or certificate chain is represented as a
|
||||
* {@code List} of certificate value {@code String}s. Each {@code String} in the
|
||||
* {@code List} is a Base64-encoded DER PKIX certificate value.
|
||||
*
|
||||
* @return the X.509 certificate chain
|
||||
*/
|
||||
@@ -125,16 +131,6 @@ public final class JoseHeader {
|
||||
return getHeader(JoseHeaderNames.X5T_S256);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the critical headers that indicates which extensions to the JWS/JWE/JWA specifications
|
||||
* are being used that MUST be understood and processed.
|
||||
*
|
||||
* @return the critical headers
|
||||
*/
|
||||
public Set<String> getCritical() {
|
||||
return getHeader(JoseHeaderNames.CRIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type header that declares the media type of the JWS/JWE.
|
||||
*
|
||||
@@ -153,6 +149,16 @@ public final class JoseHeader {
|
||||
return getHeader(JoseHeaderNames.CTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the critical headers that indicates which extensions to the JWS/JWE/JWA specifications
|
||||
* are being used that MUST be understood and processed.
|
||||
*
|
||||
* @return the critical headers
|
||||
*/
|
||||
public Set<String> getCritical() {
|
||||
return getHeader(JoseHeaderNames.CRIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the headers.
|
||||
*
|
||||
@@ -185,13 +191,13 @@ public final class JoseHeader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@link JwsAlgorithm}.
|
||||
* Returns a new {@link Builder}, initialized with the provided {@link JwaAlgorithm}.
|
||||
*
|
||||
* @param jwsAlgorithm the {@link JwsAlgorithm}
|
||||
* @param jwaAlgorithm the {@link JwaAlgorithm}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withAlgorithm(JwsAlgorithm jwsAlgorithm) {
|
||||
return new Builder(jwsAlgorithm);
|
||||
public static Builder withAlgorithm(JwaAlgorithm jwaAlgorithm) {
|
||||
return new Builder(jwaAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,9 +219,8 @@ public final class JoseHeader {
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
private Builder(JwsAlgorithm jwsAlgorithm) {
|
||||
Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
|
||||
header(JoseHeaderNames.ALG, jwsAlgorithm);
|
||||
private Builder(JwaAlgorithm jwaAlgorithm) {
|
||||
algorithm(jwaAlgorithm);
|
||||
}
|
||||
|
||||
private Builder(JoseHeader headers) {
|
||||
@@ -224,24 +229,25 @@ public final class JoseHeader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link JwsAlgorithm JWS algorithm} used to digitally sign the JWS.
|
||||
* Sets the {@link JwaAlgorithm JWA algorithm} used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @param jwsAlgorithm the JWS algorithm
|
||||
* @param jwaAlgorithm the {@link JwaAlgorithm}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder jwsAlgorithm(JwsAlgorithm jwsAlgorithm) {
|
||||
return header(JoseHeaderNames.ALG, jwsAlgorithm);
|
||||
public Builder algorithm(JwaAlgorithm jwaAlgorithm) {
|
||||
Assert.notNull(jwaAlgorithm, "jwaAlgorithm cannot be null");
|
||||
return header(JoseHeaderNames.ALG, jwaAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JWK Set URL that refers to the resource of a set of JSON-encoded public keys,
|
||||
* one of which corresponds to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @param jwkSetUri the JWK Set URL
|
||||
* @param jwkSetUrl the JWK Set URL
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder jwkSetUri(String jwkSetUri) {
|
||||
return header(JoseHeaderNames.JKU, jwkSetUri);
|
||||
public Builder jwkSetUrl(String jwkSetUrl) {
|
||||
return header(JoseHeaderNames.JKU, convertAsURL(JoseHeaderNames.JKU, jwkSetUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,16 +275,19 @@ public final class JoseHeader {
|
||||
* Sets the X.509 URL that refers to the resource for the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
*
|
||||
* @param x509Uri the X.509 URL
|
||||
* @param x509Url the X.509 URL
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509Uri(String x509Uri) {
|
||||
return header(JoseHeaderNames.X5U, x509Uri);
|
||||
public Builder x509Url(String x509Url) {
|
||||
return header(JoseHeaderNames.X5U, convertAsURL(JoseHeaderNames.X5U, x509Url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the X.509 certificate chain that contains the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
* or certificate chain corresponding to the key used to digitally sign the JWS or
|
||||
* encrypt the JWE. The certificate or certificate chain is represented as a
|
||||
* {@code List} of certificate value {@code String}s. Each {@code String} in the
|
||||
* {@code List} is a Base64-encoded DER PKIX certificate value.
|
||||
*
|
||||
* @param x509CertificateChain the X.509 certificate chain
|
||||
* @return the {@link Builder}
|
||||
@@ -309,17 +318,6 @@ public final class JoseHeader {
|
||||
return header(JoseHeaderNames.X5T_S256, x509SHA256Thumbprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the critical headers that indicates which extensions to the JWS/JWE/JWA specifications
|
||||
* are being used that MUST be understood and processed.
|
||||
*
|
||||
* @param headerNames the critical header names
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder critical(Set<String> headerNames) {
|
||||
return header(JoseHeaderNames.CRIT, headerNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type header that declares the media type of the JWS/JWE.
|
||||
*
|
||||
@@ -340,6 +338,17 @@ public final class JoseHeader {
|
||||
return header(JoseHeaderNames.CTY, contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the critical headers that indicates which extensions to the JWS/JWE/JWA specifications
|
||||
* are being used that MUST be understood and processed.
|
||||
*
|
||||
* @param headerNames the critical header names
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder critical(Set<String> headerNames) {
|
||||
return header(JoseHeaderNames.CRIT, headerNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the header.
|
||||
*
|
||||
@@ -373,19 +382,15 @@ public final class JoseHeader {
|
||||
*/
|
||||
public JoseHeader build() {
|
||||
Assert.notEmpty(this.headers, "headers cannot be empty");
|
||||
convertAsURL(JoseHeaderNames.JKU);
|
||||
convertAsURL(JoseHeaderNames.X5U);
|
||||
return new JoseHeader(this.headers);
|
||||
}
|
||||
|
||||
private void convertAsURL(String header) {
|
||||
Object value = this.headers.get(header);
|
||||
if (value != null) {
|
||||
URL convertedValue = ClaimConversionService.getSharedInstance().convert(value, URL.class);
|
||||
Assert.isTrue(convertedValue != null,
|
||||
() -> "Unable to convert header '" + header + "' of type '" + value.getClass() + "' to URL.");
|
||||
this.headers.put(header, convertedValue);
|
||||
}
|
||||
private static URL convertAsURL(String header, String value) {
|
||||
URL convertedValue = ClaimConversionService.getSharedInstance().convert(value, URL.class);
|
||||
Assert.isTrue(convertedValue != null,
|
||||
() -> "Unable to convert header '" + header + "' of type '" + value.getClass() + "' to URL.");
|
||||
return convertedValue;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,7 +27,9 @@ package org.springframework.security.oauth2.jwt;
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-5">JWT JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-4">JWS JOSE Header</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-4">JWE JOSE Header</a>
|
||||
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
|
||||
*/
|
||||
@Deprecated
|
||||
public final class JoseHeaderNames {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -22,6 +23,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -32,8 +34,10 @@ import org.springframework.util.Assert;
|
||||
* @since 0.0.1
|
||||
* @see Jwt
|
||||
* @see JwtClaimAccessor
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4">JWT Claims Set</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7519#section-4">JWT Claims Set</a>
|
||||
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
|
||||
*/
|
||||
@Deprecated
|
||||
public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
@@ -166,10 +170,10 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} to be provided access to the claims set
|
||||
* A {@code Consumer} to be provided access to the claims
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param claimsConsumer a {@code Consumer} of the claims set
|
||||
* @param claimsConsumer a {@code Consumer} of the claims
|
||||
*/
|
||||
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
|
||||
claimsConsumer.accept(this.claims);
|
||||
@@ -183,6 +187,17 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
*/
|
||||
public JwtClaimsSet build() {
|
||||
Assert.notEmpty(this.claims, "claims cannot be empty");
|
||||
|
||||
// The value of the 'iss' claim is a String or URL (StringOrURI).
|
||||
// Attempt to convert to URL.
|
||||
Object issuer = this.claims.get(JwtClaimNames.ISS);
|
||||
if (issuer != null) {
|
||||
URL convertedValue = ClaimConversionService.getSharedInstance().convert(issuer, URL.class);
|
||||
if (convertedValue != null) {
|
||||
this.claims.put(JwtClaimNames.ISS, convertedValue);
|
||||
}
|
||||
}
|
||||
|
||||
return new JwtClaimsSet(this.claims);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,12 +32,14 @@ package org.springframework.security.oauth2.jwt;
|
||||
* @see JoseHeader
|
||||
* @see JwtClaimsSet
|
||||
* @see JwtDecoder
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS Compact Serialization</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-3.1">JWE Compact Serialization</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7519">JSON Web Token (JWT)</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7515">JSON Web Signature (JWS)</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7516">JSON Web Encryption (JWE)</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7515#section-3.1">JWS Compact Serialization</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7516#section-3.1">JWE Compact Serialization</a>
|
||||
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface JwtEncoder {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,7 +21,9 @@ package org.springframework.security.oauth2.jwt;
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
|
||||
*/
|
||||
@Deprecated
|
||||
public class JwtEncodingException extends JwtException {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,26 +15,28 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JOSEObjectType;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.JWSSigner;
|
||||
import com.nimbusds.jose.KeySourceException;
|
||||
import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKMatcher;
|
||||
import com.nimbusds.jose.jwk.JWKSelector;
|
||||
import com.nimbusds.jose.jwk.KeyType;
|
||||
import com.nimbusds.jose.jwk.KeyUse;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import com.nimbusds.jose.produce.JWSSignerFactory;
|
||||
@@ -43,7 +45,6 @@ import com.nimbusds.jose.util.Base64URL;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
|
||||
import net.minidev.json.JSONObject;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -63,27 +64,19 @@ import org.springframework.util.StringUtils;
|
||||
* @see JwtEncoder
|
||||
* @see com.nimbusds.jose.jwk.source.JWKSource
|
||||
* @see com.nimbusds.jose.jwk.JWK
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token
|
||||
* (JWT)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature
|
||||
* (JWS)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS
|
||||
* Compact Serialization</a>
|
||||
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus
|
||||
* JOSE + JWT SDK</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7519">JSON Web Token (JWT)</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7515">JSON Web Signature (JWS)</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7515#section-3.1">JWS Compact Serialization</a>
|
||||
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus JOSE + JWT SDK</a>
|
||||
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
|
||||
*/
|
||||
@Deprecated
|
||||
public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
|
||||
private static final String ENCODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to encode the Jwt: %s";
|
||||
|
||||
private static final Converter<JoseHeader, JWSHeader> JWS_HEADER_CONVERTER = new JwsHeaderConverter();
|
||||
|
||||
private static final Converter<JwtClaimsSet, JWTClaimsSet> JWT_CLAIMS_SET_CONVERTER = new JwtClaimsSetConverter();
|
||||
|
||||
private static final JWSSignerFactory JWS_SIGNER_FACTORY = new DefaultJWSSignerFactory();
|
||||
|
||||
private final Map<JWK, JWSSigner> jwsSigners = new ConcurrentHashMap<>();
|
||||
|
||||
private final JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
/**
|
||||
@@ -101,108 +94,126 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
Assert.notNull(claims, "claims cannot be null");
|
||||
|
||||
JWK jwk = selectJwk(headers);
|
||||
if (jwk == null) {
|
||||
throw new JwtEncodingException(
|
||||
String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to select a JWK signing key"));
|
||||
}
|
||||
else if (!StringUtils.hasText(jwk.getKeyID())) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"The \"kid\" (key ID) from the selected JWK cannot be empty"));
|
||||
}
|
||||
headers = addKeyIdentifierHeadersIfNecessary(headers, jwk);
|
||||
|
||||
// @formatter:off
|
||||
headers = JoseHeader.from(headers)
|
||||
.type(JOSEObjectType.JWT.getType())
|
||||
.keyId(jwk.getKeyID())
|
||||
.build();
|
||||
claims = JwtClaimsSet.from(claims)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
JWSHeader jwsHeader = JWS_HEADER_CONVERTER.convert(headers);
|
||||
JWTClaimsSet jwtClaimsSet = JWT_CLAIMS_SET_CONVERTER.convert(claims);
|
||||
|
||||
JWSSigner jwsSigner = this.jwsSigners.computeIfAbsent(jwk, (key) -> {
|
||||
try {
|
||||
return JWS_SIGNER_FACTORY.createJWSSigner(key);
|
||||
}
|
||||
catch (JOSEException ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to create a JWS Signer -> " + ex.getMessage()), ex);
|
||||
}
|
||||
});
|
||||
|
||||
SignedJWT signedJwt = new SignedJWT(jwsHeader, jwtClaimsSet);
|
||||
try {
|
||||
signedJwt.sign(jwsSigner);
|
||||
}
|
||||
catch (JOSEException ex) {
|
||||
throw new JwtEncodingException(
|
||||
String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to sign the JWT -> " + ex.getMessage()), ex);
|
||||
}
|
||||
String jws = signedJwt.serialize();
|
||||
String jws = serialize(headers, claims, jwk);
|
||||
|
||||
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(), headers.getHeaders(), claims.getClaims());
|
||||
}
|
||||
|
||||
private JWK selectJwk(JoseHeader headers) {
|
||||
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(headers.getJwsAlgorithm().getName());
|
||||
JWSHeader jwsHeader = new JWSHeader(jwsAlgorithm);
|
||||
JWKSelector jwkSelector = new JWKSelector(JWKMatcher.forJWSHeader(jwsHeader));
|
||||
|
||||
List<JWK> jwks;
|
||||
try {
|
||||
JWKSelector jwkSelector = new JWKSelector(createJwkMatcher(headers));
|
||||
jwks = this.jwkSource.get(jwkSelector, null);
|
||||
}
|
||||
catch (KeySourceException ex) {
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to select a JWK signing key -> " + ex.getMessage()), ex);
|
||||
}
|
||||
|
||||
if (jwks.size() > 1) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Found multiple JWK signing keys for algorithm '" + jwsAlgorithm.getName() + "'"));
|
||||
"Found multiple JWK signing keys for algorithm '" + headers.getAlgorithm().getName() + "'"));
|
||||
}
|
||||
|
||||
return !jwks.isEmpty() ? jwks.get(0) : null;
|
||||
if (jwks.isEmpty()) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to select a JWK signing key"));
|
||||
}
|
||||
|
||||
return jwks.get(0);
|
||||
}
|
||||
|
||||
private String serialize(JoseHeader headers, JwtClaimsSet claims, JWK jwk) {
|
||||
JWSHeader jwsHeader = JWS_HEADER_CONVERTER.convert(headers);
|
||||
JWTClaimsSet jwtClaimsSet = JWT_CLAIMS_SET_CONVERTER.convert(claims);
|
||||
|
||||
JWSSigner jwsSigner = this.jwsSigners.computeIfAbsent(jwk, NimbusJwsEncoder::createSigner);
|
||||
|
||||
SignedJWT signedJwt = new SignedJWT(jwsHeader, jwtClaimsSet);
|
||||
try {
|
||||
signedJwt.sign(jwsSigner);
|
||||
} catch (JOSEException ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to sign the JWT -> " + ex.getMessage()), ex);
|
||||
}
|
||||
return signedJwt.serialize();
|
||||
}
|
||||
|
||||
private static JWKMatcher createJwkMatcher(JoseHeader headers) {
|
||||
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(headers.getAlgorithm().getName());
|
||||
|
||||
if (JWSAlgorithm.Family.RSA.contains(jwsAlgorithm) || JWSAlgorithm.Family.EC.contains(jwsAlgorithm)) {
|
||||
// @formatter:off
|
||||
return new JWKMatcher.Builder()
|
||||
.keyType(KeyType.forAlgorithm(jwsAlgorithm))
|
||||
.keyID(headers.getKeyId())
|
||||
.keyUses(KeyUse.SIGNATURE, null)
|
||||
.algorithms(jwsAlgorithm, null)
|
||||
.x509CertSHA256Thumbprint(Base64URL.from(headers.getX509SHA256Thumbprint()))
|
||||
.build();
|
||||
// @formatter:on
|
||||
} else if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) {
|
||||
// @formatter:off
|
||||
return new JWKMatcher.Builder()
|
||||
.keyType(KeyType.forAlgorithm(jwsAlgorithm))
|
||||
.keyID(headers.getKeyId())
|
||||
.privateOnly(true)
|
||||
.algorithms(jwsAlgorithm, null)
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JoseHeader addKeyIdentifierHeadersIfNecessary(JoseHeader headers, JWK jwk) {
|
||||
// Check if headers have already been added
|
||||
if (StringUtils.hasText(headers.getKeyId()) && StringUtils.hasText(headers.getX509SHA256Thumbprint())) {
|
||||
return headers;
|
||||
}
|
||||
// Check if headers can be added from JWK
|
||||
if (!StringUtils.hasText(jwk.getKeyID()) && jwk.getX509CertSHA256Thumbprint() == null) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
JoseHeader.Builder headersBuilder = JoseHeader.from(headers);
|
||||
if (!StringUtils.hasText(headers.getKeyId()) && StringUtils.hasText(jwk.getKeyID())) {
|
||||
headersBuilder.keyId(jwk.getKeyID());
|
||||
}
|
||||
if (!StringUtils.hasText(headers.getX509SHA256Thumbprint()) && jwk.getX509CertSHA256Thumbprint() != null) {
|
||||
headersBuilder.x509SHA256Thumbprint(jwk.getX509CertSHA256Thumbprint().toString());
|
||||
}
|
||||
|
||||
return headersBuilder.build();
|
||||
}
|
||||
|
||||
private static JWSSigner createSigner(JWK jwk) {
|
||||
try {
|
||||
return JWS_SIGNER_FACTORY.createJWSSigner(jwk);
|
||||
} catch (JOSEException ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to create a JWS Signer -> " + ex.getMessage()), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static class JwsHeaderConverter implements Converter<JoseHeader, JWSHeader> {
|
||||
|
||||
@Override
|
||||
public JWSHeader convert(JoseHeader headers) {
|
||||
JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(headers.getJwsAlgorithm().getName()));
|
||||
JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(headers.getAlgorithm().getName()));
|
||||
|
||||
Set<String> critical = headers.getCritical();
|
||||
if (!CollectionUtils.isEmpty(critical)) {
|
||||
builder.criticalParams(critical);
|
||||
}
|
||||
|
||||
String contentType = headers.getContentType();
|
||||
if (StringUtils.hasText(contentType)) {
|
||||
builder.contentType(contentType);
|
||||
}
|
||||
|
||||
URL jwkSetUri = headers.getJwkSetUri();
|
||||
if (jwkSetUri != null) {
|
||||
try {
|
||||
builder.jwkURL(jwkSetUri.toURI());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.JKU + "' JOSE header to a URI"), ex);
|
||||
}
|
||||
if (headers.getJwkSetUrl() != null) {
|
||||
builder.jwkURL(convertAsURI(JoseHeaderNames.JKU, headers.getJwkSetUrl()));
|
||||
}
|
||||
|
||||
Map<String, Object> jwk = headers.getJwk();
|
||||
if (!CollectionUtils.isEmpty(jwk)) {
|
||||
try {
|
||||
builder.jwk(JWK.parse(new JSONObject(jwk)));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
builder.jwk(JWK.parse(jwk));
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex);
|
||||
"Unable to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,14 +222,17 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
builder.keyID(keyId);
|
||||
}
|
||||
|
||||
String type = headers.getType();
|
||||
if (StringUtils.hasText(type)) {
|
||||
builder.type(new JOSEObjectType(type));
|
||||
if (headers.getX509Url() != null) {
|
||||
builder.x509CertURL(convertAsURI(JoseHeaderNames.X5U, headers.getX509Url()));
|
||||
}
|
||||
|
||||
List<String> x509CertificateChain = headers.getX509CertificateChain();
|
||||
if (!CollectionUtils.isEmpty(x509CertificateChain)) {
|
||||
builder.x509CertChain(x509CertificateChain.stream().map(Base64::new).collect(Collectors.toList()));
|
||||
List<Base64> x5cList = new ArrayList<>();
|
||||
x509CertificateChain.forEach((x5c) -> x5cList.add(new Base64(x5c)));
|
||||
if (!x5cList.isEmpty()) {
|
||||
builder.x509CertChain(x5cList);
|
||||
}
|
||||
}
|
||||
|
||||
String x509SHA1Thumbprint = headers.getX509SHA1Thumbprint();
|
||||
@@ -231,27 +245,43 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
builder.x509CertSHA256Thumbprint(new Base64URL(x509SHA256Thumbprint));
|
||||
}
|
||||
|
||||
URL x509Uri = headers.getX509Uri();
|
||||
if (x509Uri != null) {
|
||||
try {
|
||||
builder.x509CertURL(x509Uri.toURI());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.X5U + "' JOSE header to a URI"), ex);
|
||||
}
|
||||
String type = headers.getType();
|
||||
if (StringUtils.hasText(type)) {
|
||||
builder.type(new JOSEObjectType(type));
|
||||
}
|
||||
|
||||
Map<String, Object> customHeaders = headers.getHeaders().entrySet().stream()
|
||||
.filter((header) -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
if (!CollectionUtils.isEmpty(customHeaders)) {
|
||||
String contentType = headers.getContentType();
|
||||
if (StringUtils.hasText(contentType)) {
|
||||
builder.contentType(contentType);
|
||||
}
|
||||
|
||||
Set<String> critical = headers.getCritical();
|
||||
if (!CollectionUtils.isEmpty(critical)) {
|
||||
builder.criticalParams(critical);
|
||||
}
|
||||
|
||||
Map<String, Object> customHeaders = new HashMap<>();
|
||||
headers.getHeaders().forEach((name, value) -> {
|
||||
if (!JWSHeader.getRegisteredParameterNames().contains(name)) {
|
||||
customHeaders.put(name, value);
|
||||
}
|
||||
});
|
||||
if (!customHeaders.isEmpty()) {
|
||||
builder.customParams(customHeaders);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static URI convertAsURI(String header, URL url) {
|
||||
try {
|
||||
return url.toURI();
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Unable to convert '" + header + "' JOSE header to a URI"), ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class JwtClaimsSetConverter implements Converter<JwtClaimsSet, JWTClaimsSet> {
|
||||
@@ -260,9 +290,10 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
public JWTClaimsSet convert(JwtClaimsSet claims) {
|
||||
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
|
||||
|
||||
URL issuer = claims.getIssuer();
|
||||
// NOTE: The value of the 'iss' claim is a String or URL (StringOrURI).
|
||||
Object issuer = claims.getClaim(JwtClaimNames.ISS);
|
||||
if (issuer != null) {
|
||||
builder.issuer(issuer.toExternalForm());
|
||||
builder.issuer(issuer.toString());
|
||||
}
|
||||
|
||||
String subject = claims.getSubject();
|
||||
@@ -275,11 +306,6 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
builder.audience(audience);
|
||||
}
|
||||
|
||||
Instant issuedAt = claims.getIssuedAt();
|
||||
if (issuedAt != null) {
|
||||
builder.issueTime(Date.from(issuedAt));
|
||||
}
|
||||
|
||||
Instant expiresAt = claims.getExpiresAt();
|
||||
if (expiresAt != null) {
|
||||
builder.expirationTime(Date.from(expiresAt));
|
||||
@@ -290,15 +316,23 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
||||
builder.notBeforeTime(Date.from(notBefore));
|
||||
}
|
||||
|
||||
Instant issuedAt = claims.getIssuedAt();
|
||||
if (issuedAt != null) {
|
||||
builder.issueTime(Date.from(issuedAt));
|
||||
}
|
||||
|
||||
String jwtId = claims.getId();
|
||||
if (StringUtils.hasText(jwtId)) {
|
||||
builder.jwtID(jwtId);
|
||||
}
|
||||
|
||||
Map<String, Object> customClaims = claims.getClaims().entrySet().stream()
|
||||
.filter((claim) -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
if (!CollectionUtils.isEmpty(customClaims)) {
|
||||
Map<String, Object> customClaims = new HashMap<>();
|
||||
claims.getClaims().forEach((name, value) -> {
|
||||
if (!JWTClaimsSet.getRegisteredNames().contains(name)) {
|
||||
customClaims.put(name, value);
|
||||
}
|
||||
});
|
||||
if (!customClaims.isEmpty()) {
|
||||
customClaims.forEach(builder::claim);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2AuthorizationConsentService} that stores {@link OAuth2AuthorizationConsent}'s in-memory.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationConsentService
|
||||
*/
|
||||
public final class InMemoryOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
|
||||
private final Map<Integer, OAuth2AuthorizationConsent> authorizationConsents = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService}.
|
||||
*/
|
||||
public InMemoryOAuth2AuthorizationConsentService() {
|
||||
this(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService} using the provided parameters.
|
||||
*
|
||||
* @param authorizationConsents the authorization consent(s)
|
||||
*/
|
||||
public InMemoryOAuth2AuthorizationConsentService(OAuth2AuthorizationConsent... authorizationConsents) {
|
||||
this(Arrays.asList(authorizationConsents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService} using the provided parameters.
|
||||
*
|
||||
* @param authorizationConsents the authorization consent(s)
|
||||
*/
|
||||
public InMemoryOAuth2AuthorizationConsentService(List<OAuth2AuthorizationConsent> authorizationConsents) {
|
||||
Assert.notNull(authorizationConsents, "authorizationConsents cannot be null");
|
||||
authorizationConsents.forEach(authorizationConsent -> {
|
||||
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||
int id = getId(authorizationConsent);
|
||||
Assert.isTrue(!this.authorizationConsents.containsKey(id),
|
||||
"The authorizationConsent must be unique. Found duplicate, with registered client id: ["
|
||||
+ authorizationConsent.getRegisteredClientId()
|
||||
+ "] and principal name: [" + authorizationConsent.getPrincipalName() + "]");
|
||||
this.authorizationConsents.put(id, authorizationConsent);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||
int id = getId(authorizationConsent);
|
||||
this.authorizationConsents.put(id, authorizationConsent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||
int id = getId(authorizationConsent);
|
||||
this.authorizationConsents.remove(id, authorizationConsent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
|
||||
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
|
||||
Assert.hasText(principalName, "principalName cannot be empty");
|
||||
int id = getId(registeredClientId, principalName);
|
||||
return this.authorizationConsents.get(id);
|
||||
}
|
||||
|
||||
private static int getId(String registeredClientId, String principalName) {
|
||||
return Objects.hash(registeredClientId, principalName);
|
||||
}
|
||||
|
||||
private static int getId(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
return getId(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,12 +17,14 @@ package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
@@ -40,8 +42,29 @@ import org.springframework.util.Assert;
|
||||
* @see OAuth2AuthorizationService
|
||||
*/
|
||||
public final class InMemoryOAuth2AuthorizationService implements OAuth2AuthorizationService {
|
||||
private int maxInitializedAuthorizations = 100;
|
||||
|
||||
/*
|
||||
* Stores "initialized" (uncompleted) authorizations, where an access token has not yet been granted.
|
||||
* This state occurs with the authorization_code grant flow during the user consent step OR
|
||||
* when the code is returned in the authorization response but the access token request is not yet initiated.
|
||||
*/
|
||||
private Map<String, OAuth2Authorization> initializedAuthorizations =
|
||||
Collections.synchronizedMap(new MaxSizeHashMap<>(this.maxInitializedAuthorizations));
|
||||
|
||||
/*
|
||||
* Stores "completed" authorizations, where an access token has been granted.
|
||||
*/
|
||||
private final Map<String, OAuth2Authorization> authorizations = new ConcurrentHashMap<>();
|
||||
|
||||
/*
|
||||
* Constructor used for testing only.
|
||||
*/
|
||||
InMemoryOAuth2AuthorizationService(int maxInitializedAuthorizations) {
|
||||
this.maxInitializedAuthorizations = maxInitializedAuthorizations;
|
||||
this.initializedAuthorizations = Collections.synchronizedMap(new MaxSizeHashMap<>(this.maxInitializedAuthorizations));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code InMemoryOAuth2AuthorizationService}.
|
||||
*/
|
||||
@@ -76,30 +99,52 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
|
||||
@Override
|
||||
public void save(OAuth2Authorization authorization) {
|
||||
Assert.notNull(authorization, "authorization cannot be null");
|
||||
this.authorizations.put(authorization.getId(), authorization);
|
||||
if (isComplete(authorization)) {
|
||||
this.authorizations.put(authorization.getId(), authorization);
|
||||
} else {
|
||||
this.initializedAuthorizations.put(authorization.getId(), authorization);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(OAuth2Authorization authorization) {
|
||||
Assert.notNull(authorization, "authorization cannot be null");
|
||||
this.authorizations.remove(authorization.getId(), authorization);
|
||||
if (isComplete(authorization)) {
|
||||
this.authorizations.remove(authorization.getId(), authorization);
|
||||
} else {
|
||||
this.initializedAuthorizations.remove(authorization.getId(), authorization);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2Authorization findById(String id) {
|
||||
Assert.hasText(id, "id cannot be empty");
|
||||
return this.authorizations.get(id);
|
||||
OAuth2Authorization authorization = this.authorizations.get(id);
|
||||
return authorization != null ?
|
||||
authorization :
|
||||
this.initializedAuthorizations.get(id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
|
||||
Assert.hasText(token, "token cannot be empty");
|
||||
return this.authorizations.values().stream()
|
||||
.filter(authorization -> hasToken(authorization, token, tokenType))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
for (OAuth2Authorization authorization : this.authorizations.values()) {
|
||||
if (hasToken(authorization, token, tokenType)) {
|
||||
return authorization;
|
||||
}
|
||||
}
|
||||
for (OAuth2Authorization authorization : this.initializedAuthorizations.values()) {
|
||||
if (hasToken(authorization, token, tokenType)) {
|
||||
return authorization;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isComplete(OAuth2Authorization authorization) {
|
||||
return authorization.getAccessToken() != null;
|
||||
}
|
||||
|
||||
private static boolean hasToken(OAuth2Authorization authorization, String token, @Nullable OAuth2TokenType tokenType) {
|
||||
@@ -141,4 +186,19 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
|
||||
authorization.getToken(OAuth2RefreshToken.class);
|
||||
return refreshToken != null && refreshToken.getToken().getTokenValue().equals(token);
|
||||
}
|
||||
|
||||
private static final class MaxSizeHashMap<K, V> extends LinkedHashMap<K, V> {
|
||||
private final int maxSize;
|
||||
|
||||
private MaxSizeHashMap(int maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
|
||||
return size() > this.maxSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.JdbcOperations;
|
||||
import org.springframework.jdbc.core.PreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.core.SqlParameterValue;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A JDBC implementation of an {@link OAuth2AuthorizationConsentService} that uses a
|
||||
* {@link JdbcOperations} for {@link OAuth2AuthorizationConsent} persistence.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This {@code OAuth2AuthorizationConsentService} depends on the table definition
|
||||
* described in
|
||||
* "classpath:org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql" and
|
||||
* therefore MUST be defined in the database schema.
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationConsentService
|
||||
* @see OAuth2AuthorizationConsent
|
||||
* @see JdbcOperations
|
||||
* @see RowMapper
|
||||
*/
|
||||
public class JdbcOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
|
||||
|
||||
// @formatter:off
|
||||
private static final String COLUMN_NAMES = "registered_client_id, "
|
||||
+ "principal_name, "
|
||||
+ "authorities";
|
||||
// @formatter:on
|
||||
|
||||
private static final String TABLE_NAME = "oauth2_authorization_consent";
|
||||
|
||||
private static final String PK_FILTER = "registered_client_id = ? AND principal_name = ?";
|
||||
|
||||
// @formatter:off
|
||||
private static final String LOAD_AUTHORIZATION_CONSENT_SQL = "SELECT " + COLUMN_NAMES
|
||||
+ " FROM " + TABLE_NAME
|
||||
+ " WHERE " + PK_FILTER;
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String SAVE_AUTHORIZATION_CONSENT_SQL = "INSERT INTO " + TABLE_NAME
|
||||
+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?)";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String UPDATE_AUTHORIZATION_CONSENT_SQL = "UPDATE " + TABLE_NAME
|
||||
+ " SET authorities = ?"
|
||||
+ " WHERE " + PK_FILTER;
|
||||
// @formatter:on
|
||||
|
||||
private static final String REMOVE_AUTHORIZATION_CONSENT_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + PK_FILTER;
|
||||
|
||||
private final JdbcOperations jdbcOperations;
|
||||
private RowMapper<OAuth2AuthorizationConsent> authorizationConsentRowMapper;
|
||||
private Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> authorizationConsentParametersMapper;
|
||||
|
||||
/**
|
||||
* Constructs a {@code JdbcOAuth2AuthorizationConsentService} using the provided parameters.
|
||||
*
|
||||
* @param jdbcOperations the JDBC operations
|
||||
* @param registeredClientRepository the registered client repository
|
||||
*/
|
||||
public JdbcOAuth2AuthorizationConsentService(JdbcOperations jdbcOperations,
|
||||
RegisteredClientRepository registeredClientRepository) {
|
||||
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
this.jdbcOperations = jdbcOperations;
|
||||
this.authorizationConsentRowMapper = new OAuth2AuthorizationConsentRowMapper(registeredClientRepository);
|
||||
this.authorizationConsentParametersMapper = new OAuth2AuthorizationConsentParametersMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||
OAuth2AuthorizationConsent existingAuthorizationConsent = findById(
|
||||
authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
|
||||
if (existingAuthorizationConsent == null) {
|
||||
insertAuthorizationConsent(authorizationConsent);
|
||||
} else {
|
||||
updateAuthorizationConsent(authorizationConsent);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAuthorizationConsent(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
List<SqlParameterValue> parameters = this.authorizationConsentParametersMapper.apply(authorizationConsent);
|
||||
SqlParameterValue registeredClientId = parameters.remove(0);
|
||||
SqlParameterValue principalName = parameters.remove(0);
|
||||
parameters.add(registeredClientId);
|
||||
parameters.add(principalName);
|
||||
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
|
||||
this.jdbcOperations.update(UPDATE_AUTHORIZATION_CONSENT_SQL, pss);
|
||||
}
|
||||
|
||||
private void insertAuthorizationConsent(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
List<SqlParameterValue> parameters = this.authorizationConsentParametersMapper.apply(authorizationConsent);
|
||||
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
|
||||
this.jdbcOperations.update(SAVE_AUTHORIZATION_CONSENT_SQL, pss);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||
SqlParameterValue[] parameters = new SqlParameterValue[] {
|
||||
new SqlParameterValue(Types.VARCHAR, authorizationConsent.getRegisteredClientId()),
|
||||
new SqlParameterValue(Types.VARCHAR, authorizationConsent.getPrincipalName())
|
||||
};
|
||||
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
|
||||
this.jdbcOperations.update(REMOVE_AUTHORIZATION_CONSENT_SQL, pss);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
|
||||
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
|
||||
Assert.hasText(principalName, "principalName cannot be empty");
|
||||
SqlParameterValue[] parameters = new SqlParameterValue[] {
|
||||
new SqlParameterValue(Types.VARCHAR, registeredClientId),
|
||||
new SqlParameterValue(Types.VARCHAR, principalName)};
|
||||
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
|
||||
List<OAuth2AuthorizationConsent> result = this.jdbcOperations.query(LOAD_AUTHORIZATION_CONSENT_SQL, pss,
|
||||
this.authorizationConsentRowMapper);
|
||||
return !result.isEmpty() ? result.get(0) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RowMapper} used for mapping the current row in
|
||||
* {@code java.sql.ResultSet} to {@link OAuth2AuthorizationConsent}. The default is
|
||||
* {@link OAuth2AuthorizationConsentRowMapper}.
|
||||
*
|
||||
* @param authorizationConsentRowMapper the {@link RowMapper} used for mapping the current
|
||||
* row in {@code ResultSet} to {@link OAuth2AuthorizationConsent}
|
||||
*/
|
||||
public final void setAuthorizationConsentRowMapper(RowMapper<OAuth2AuthorizationConsent> authorizationConsentRowMapper) {
|
||||
Assert.notNull(authorizationConsentRowMapper, "authorizationConsentRowMapper cannot be null");
|
||||
this.authorizationConsentRowMapper = authorizationConsentRowMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Function} used for mapping {@link OAuth2AuthorizationConsent} to
|
||||
* a {@code List} of {@link SqlParameterValue}. The default is
|
||||
* {@link OAuth2AuthorizationConsentParametersMapper}.
|
||||
*
|
||||
* @param authorizationConsentParametersMapper the {@code Function} used for mapping
|
||||
* {@link OAuth2AuthorizationConsent} to a {@code List} of {@link SqlParameterValue}
|
||||
*/
|
||||
public final void setAuthorizationConsentParametersMapper(
|
||||
Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> authorizationConsentParametersMapper) {
|
||||
Assert.notNull(authorizationConsentParametersMapper, "authorizationConsentParametersMapper cannot be null");
|
||||
this.authorizationConsentParametersMapper = authorizationConsentParametersMapper;
|
||||
}
|
||||
|
||||
protected final JdbcOperations getJdbcOperations() {
|
||||
return this.jdbcOperations;
|
||||
}
|
||||
|
||||
protected final RowMapper<OAuth2AuthorizationConsent> getAuthorizationConsentRowMapper() {
|
||||
return this.authorizationConsentRowMapper;
|
||||
}
|
||||
|
||||
protected final Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> getAuthorizationConsentParametersMapper() {
|
||||
return this.authorizationConsentParametersMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default {@link RowMapper} that maps the current row in
|
||||
* {@code ResultSet} to {@link OAuth2AuthorizationConsent}.
|
||||
*/
|
||||
public static class OAuth2AuthorizationConsentRowMapper implements RowMapper<OAuth2AuthorizationConsent> {
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
public OAuth2AuthorizationConsentRowMapper(RegisteredClientRepository registeredClientRepository) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizationConsent mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
String registeredClientId = rs.getString("registered_client_id");
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findById(registeredClientId);
|
||||
if (registeredClient == null) {
|
||||
throw new DataRetrievalFailureException(
|
||||
"The RegisteredClient with id '" + registeredClientId + "' was not found in the RegisteredClientRepository.");
|
||||
}
|
||||
|
||||
String principalName = rs.getString("principal_name");
|
||||
|
||||
OAuth2AuthorizationConsent.Builder builder = OAuth2AuthorizationConsent.withId(registeredClientId, principalName);
|
||||
String authorizationConsentAuthorities = rs.getString("authorities");
|
||||
if (authorizationConsentAuthorities != null) {
|
||||
for (String authority : StringUtils.commaDelimitedListToSet(authorizationConsentAuthorities)) {
|
||||
builder.authority(new SimpleGrantedAuthority(authority));
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
protected final RegisteredClientRepository getRegisteredClientRepository() {
|
||||
return this.registeredClientRepository;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The default {@code Function} that maps {@link OAuth2AuthorizationConsent} to a
|
||||
* {@code List} of {@link SqlParameterValue}.
|
||||
*/
|
||||
public static class OAuth2AuthorizationConsentParametersMapper implements Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> {
|
||||
|
||||
@Override
|
||||
public List<SqlParameterValue> apply(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
List<SqlParameterValue> parameters = new ArrayList<>();
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, authorizationConsent.getRegisteredClientId()));
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, authorizationConsent.getPrincipalName()));
|
||||
|
||||
Set<String> authorities = new HashSet<>();
|
||||
for (GrantedAuthority authority : authorizationConsent.getAuthorities()) {
|
||||
authorities.add(authority.getAuthority());
|
||||
}
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToDelimitedString(authorities, ",")));
|
||||
return parameters;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,686 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.sql.Types;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.ConnectionCallback;
|
||||
import org.springframework.jdbc.core.JdbcOperations;
|
||||
import org.springframework.jdbc.core.PreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.core.SqlParameterValue;
|
||||
import org.springframework.jdbc.support.lob.DefaultLobHandler;
|
||||
import org.springframework.jdbc.support.lob.LobCreator;
|
||||
import org.springframework.jdbc.support.lob.LobHandler;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A JDBC implementation of an {@link OAuth2AuthorizationService} that uses a
|
||||
* {@link JdbcOperations} for {@link OAuth2Authorization} persistence.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This {@code OAuth2AuthorizationService} depends on the table definition
|
||||
* described in
|
||||
* "classpath:org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql" and
|
||||
* therefore MUST be defined in the database schema.
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2Authorization
|
||||
* @see JdbcOperations
|
||||
* @see RowMapper
|
||||
*/
|
||||
public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationService {
|
||||
|
||||
// @formatter:off
|
||||
private static final String COLUMN_NAMES = "id, "
|
||||
+ "registered_client_id, "
|
||||
+ "principal_name, "
|
||||
+ "authorization_grant_type, "
|
||||
+ "attributes, "
|
||||
+ "state, "
|
||||
+ "authorization_code_value, "
|
||||
+ "authorization_code_issued_at, "
|
||||
+ "authorization_code_expires_at,"
|
||||
+ "authorization_code_metadata,"
|
||||
+ "access_token_value,"
|
||||
+ "access_token_issued_at,"
|
||||
+ "access_token_expires_at,"
|
||||
+ "access_token_metadata,"
|
||||
+ "access_token_type,"
|
||||
+ "access_token_scopes,"
|
||||
+ "oidc_id_token_value,"
|
||||
+ "oidc_id_token_issued_at,"
|
||||
+ "oidc_id_token_expires_at,"
|
||||
+ "oidc_id_token_metadata,"
|
||||
+ "refresh_token_value,"
|
||||
+ "refresh_token_issued_at,"
|
||||
+ "refresh_token_expires_at,"
|
||||
+ "refresh_token_metadata";
|
||||
// @formatter:on
|
||||
|
||||
private static final String TABLE_NAME = "oauth2_authorization";
|
||||
|
||||
private static final String PK_FILTER = "id = ?";
|
||||
private static final String UNKNOWN_TOKEN_TYPE_FILTER = "state = ? OR authorization_code_value = ? OR " +
|
||||
"access_token_value = ? OR refresh_token_value = ?";
|
||||
|
||||
private static final String STATE_FILTER = "state = ?";
|
||||
private static final String AUTHORIZATION_CODE_FILTER = "authorization_code_value = ?";
|
||||
private static final String ACCESS_TOKEN_FILTER = "access_token_value = ?";
|
||||
private static final String REFRESH_TOKEN_FILTER = "refresh_token_value = ?";
|
||||
|
||||
// @formatter:off
|
||||
private static final String LOAD_AUTHORIZATION_SQL = "SELECT " + COLUMN_NAMES
|
||||
+ " FROM " + TABLE_NAME
|
||||
+ " WHERE ";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String SAVE_AUTHORIZATION_SQL = "INSERT INTO " + TABLE_NAME
|
||||
+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String UPDATE_AUTHORIZATION_SQL = "UPDATE " + TABLE_NAME
|
||||
+ " SET registered_client_id = ?, principal_name = ?, authorization_grant_type = ?, attributes = ?, state = ?,"
|
||||
+ " authorization_code_value = ?, authorization_code_issued_at = ?, authorization_code_expires_at = ?, authorization_code_metadata = ?,"
|
||||
+ " access_token_value = ?, access_token_issued_at = ?, access_token_expires_at = ?, access_token_metadata = ?, access_token_type = ?, access_token_scopes = ?,"
|
||||
+ " oidc_id_token_value = ?, oidc_id_token_issued_at = ?, oidc_id_token_expires_at = ?, oidc_id_token_metadata = ?,"
|
||||
+ " refresh_token_value = ?, refresh_token_issued_at = ?, refresh_token_expires_at = ?, refresh_token_metadata = ?"
|
||||
+ " WHERE " + PK_FILTER;
|
||||
// @formatter:on
|
||||
|
||||
private static final String REMOVE_AUTHORIZATION_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + PK_FILTER;
|
||||
|
||||
private static Map<String, ColumnMetadata> columnMetadataMap;
|
||||
|
||||
private final JdbcOperations jdbcOperations;
|
||||
private final LobHandler lobHandler;
|
||||
private RowMapper<OAuth2Authorization> authorizationRowMapper;
|
||||
private Function<OAuth2Authorization, List<SqlParameterValue>> authorizationParametersMapper;
|
||||
|
||||
/**
|
||||
* Constructs a {@code JdbcOAuth2AuthorizationService} using the provided parameters.
|
||||
*
|
||||
* @param jdbcOperations the JDBC operations
|
||||
* @param registeredClientRepository the registered client repository
|
||||
*/
|
||||
public JdbcOAuth2AuthorizationService(JdbcOperations jdbcOperations,
|
||||
RegisteredClientRepository registeredClientRepository) {
|
||||
this(jdbcOperations, registeredClientRepository, new DefaultLobHandler());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code JdbcOAuth2AuthorizationService} using the provided parameters.
|
||||
*
|
||||
* @param jdbcOperations the JDBC operations
|
||||
* @param registeredClientRepository the registered client repository
|
||||
* @param lobHandler the handler for large binary fields and large text fields
|
||||
*/
|
||||
public JdbcOAuth2AuthorizationService(JdbcOperations jdbcOperations,
|
||||
RegisteredClientRepository registeredClientRepository, LobHandler lobHandler) {
|
||||
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
Assert.notNull(lobHandler, "lobHandler cannot be null");
|
||||
this.jdbcOperations = jdbcOperations;
|
||||
this.lobHandler = lobHandler;
|
||||
OAuth2AuthorizationRowMapper authorizationRowMapper = new OAuth2AuthorizationRowMapper(registeredClientRepository);
|
||||
authorizationRowMapper.setLobHandler(lobHandler);
|
||||
this.authorizationRowMapper = authorizationRowMapper;
|
||||
this.authorizationParametersMapper = new OAuth2AuthorizationParametersMapper();
|
||||
initColumnMetadata(jdbcOperations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(OAuth2Authorization authorization) {
|
||||
Assert.notNull(authorization, "authorization cannot be null");
|
||||
OAuth2Authorization existingAuthorization = findById(authorization.getId());
|
||||
if (existingAuthorization == null) {
|
||||
insertAuthorization(authorization);
|
||||
} else {
|
||||
updateAuthorization(authorization);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAuthorization(OAuth2Authorization authorization) {
|
||||
List<SqlParameterValue> parameters = this.authorizationParametersMapper.apply(authorization);
|
||||
SqlParameterValue id = parameters.remove(0);
|
||||
parameters.add(id);
|
||||
try (LobCreator lobCreator = this.lobHandler.getLobCreator()) {
|
||||
PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator,
|
||||
parameters.toArray());
|
||||
this.jdbcOperations.update(UPDATE_AUTHORIZATION_SQL, pss);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertAuthorization(OAuth2Authorization authorization) {
|
||||
List<SqlParameterValue> parameters = this.authorizationParametersMapper.apply(authorization);
|
||||
try (LobCreator lobCreator = this.lobHandler.getLobCreator()) {
|
||||
PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator,
|
||||
parameters.toArray());
|
||||
this.jdbcOperations.update(SAVE_AUTHORIZATION_SQL, pss);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(OAuth2Authorization authorization) {
|
||||
Assert.notNull(authorization, "authorization cannot be null");
|
||||
SqlParameterValue[] parameters = new SqlParameterValue[] {
|
||||
new SqlParameterValue(Types.VARCHAR, authorization.getId())
|
||||
};
|
||||
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
|
||||
this.jdbcOperations.update(REMOVE_AUTHORIZATION_SQL, pss);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2Authorization findById(String id) {
|
||||
Assert.hasText(id, "id cannot be empty");
|
||||
List<SqlParameterValue> parameters = new ArrayList<>();
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, id));
|
||||
return findBy(PK_FILTER, parameters);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
|
||||
Assert.hasText(token, "token cannot be empty");
|
||||
List<SqlParameterValue> parameters = new ArrayList<>();
|
||||
if (tokenType == null) {
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, token));
|
||||
parameters.add(mapToSqlParameter("authorization_code_value", token));
|
||||
parameters.add(mapToSqlParameter("access_token_value", token));
|
||||
parameters.add(mapToSqlParameter("refresh_token_value", token));
|
||||
return findBy(UNKNOWN_TOKEN_TYPE_FILTER, parameters);
|
||||
} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, token));
|
||||
return findBy(STATE_FILTER, parameters);
|
||||
} else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
|
||||
parameters.add(mapToSqlParameter("authorization_code_value", token));
|
||||
return findBy(AUTHORIZATION_CODE_FILTER, parameters);
|
||||
} else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
|
||||
parameters.add(mapToSqlParameter("access_token_value", token));
|
||||
return findBy(ACCESS_TOKEN_FILTER, parameters);
|
||||
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
|
||||
parameters.add(mapToSqlParameter("refresh_token_value", token));
|
||||
return findBy(REFRESH_TOKEN_FILTER, parameters);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private OAuth2Authorization findBy(String filter, List<SqlParameterValue> parameters) {
|
||||
try (LobCreator lobCreator = getLobHandler().getLobCreator()) {
|
||||
PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator,
|
||||
parameters.toArray());
|
||||
List<OAuth2Authorization> result = getJdbcOperations().query(LOAD_AUTHORIZATION_SQL + filter, pss, getAuthorizationRowMapper());
|
||||
return !result.isEmpty() ? result.get(0) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RowMapper} used for mapping the current row in
|
||||
* {@code java.sql.ResultSet} to {@link OAuth2Authorization}. The default is
|
||||
* {@link OAuth2AuthorizationRowMapper}.
|
||||
*
|
||||
* @param authorizationRowMapper the {@link RowMapper} used for mapping the current
|
||||
* row in {@code ResultSet} to {@link OAuth2Authorization}
|
||||
*/
|
||||
public final void setAuthorizationRowMapper(RowMapper<OAuth2Authorization> authorizationRowMapper) {
|
||||
Assert.notNull(authorizationRowMapper, "authorizationRowMapper cannot be null");
|
||||
this.authorizationRowMapper = authorizationRowMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Function} used for mapping {@link OAuth2Authorization} to
|
||||
* a {@code List} of {@link SqlParameterValue}. The default is
|
||||
* {@link OAuth2AuthorizationParametersMapper}.
|
||||
*
|
||||
* @param authorizationParametersMapper the {@code Function} used for mapping
|
||||
* {@link OAuth2Authorization} to a {@code List} of {@link SqlParameterValue}
|
||||
*/
|
||||
public final void setAuthorizationParametersMapper(
|
||||
Function<OAuth2Authorization, List<SqlParameterValue>> authorizationParametersMapper) {
|
||||
Assert.notNull(authorizationParametersMapper, "authorizationParametersMapper cannot be null");
|
||||
this.authorizationParametersMapper = authorizationParametersMapper;
|
||||
}
|
||||
|
||||
protected final JdbcOperations getJdbcOperations() {
|
||||
return this.jdbcOperations;
|
||||
}
|
||||
|
||||
protected final LobHandler getLobHandler() {
|
||||
return this.lobHandler;
|
||||
}
|
||||
|
||||
protected final RowMapper<OAuth2Authorization> getAuthorizationRowMapper() {
|
||||
return this.authorizationRowMapper;
|
||||
}
|
||||
|
||||
protected final Function<OAuth2Authorization, List<SqlParameterValue>> getAuthorizationParametersMapper() {
|
||||
return this.authorizationParametersMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default {@link RowMapper} that maps the current row in
|
||||
* {@code java.sql.ResultSet} to {@link OAuth2Authorization}.
|
||||
*/
|
||||
public static class OAuth2AuthorizationRowMapper implements RowMapper<OAuth2Authorization> {
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private LobHandler lobHandler = new DefaultLobHandler();
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public OAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
|
||||
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
|
||||
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
|
||||
this.objectMapper.registerModules(securityModules);
|
||||
this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public OAuth2Authorization mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
String registeredClientId = rs.getString("registered_client_id");
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findById(registeredClientId);
|
||||
if (registeredClient == null) {
|
||||
throw new DataRetrievalFailureException(
|
||||
"The RegisteredClient with id '" + registeredClientId + "' was not found in the RegisteredClientRepository.");
|
||||
}
|
||||
|
||||
OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient);
|
||||
String id = rs.getString("id");
|
||||
String principalName = rs.getString("principal_name");
|
||||
String authorizationGrantType = rs.getString("authorization_grant_type");
|
||||
Map<String, Object> attributes = parseMap(getLobValue(rs, "attributes"));
|
||||
|
||||
builder.id(id)
|
||||
.principalName(principalName)
|
||||
.authorizationGrantType(new AuthorizationGrantType(authorizationGrantType))
|
||||
.attributes((attrs) -> attrs.putAll(attributes));
|
||||
|
||||
String state = rs.getString("state");
|
||||
if (StringUtils.hasText(state)) {
|
||||
builder.attribute(OAuth2ParameterNames.STATE, state);
|
||||
}
|
||||
|
||||
Instant tokenIssuedAt;
|
||||
Instant tokenExpiresAt;
|
||||
String authorizationCodeValue = getLobValue(rs, "authorization_code_value");
|
||||
|
||||
if (StringUtils.hasText(authorizationCodeValue)) {
|
||||
tokenIssuedAt = rs.getTimestamp("authorization_code_issued_at").toInstant();
|
||||
tokenExpiresAt = rs.getTimestamp("authorization_code_expires_at").toInstant();
|
||||
Map<String, Object> authorizationCodeMetadata = parseMap(getLobValue(rs, "authorization_code_metadata"));
|
||||
|
||||
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
|
||||
authorizationCodeValue, tokenIssuedAt, tokenExpiresAt);
|
||||
builder.token(authorizationCode, (metadata) -> metadata.putAll(authorizationCodeMetadata));
|
||||
}
|
||||
|
||||
String accessTokenValue = getLobValue(rs, "access_token_value");
|
||||
if (StringUtils.hasText(accessTokenValue)) {
|
||||
tokenIssuedAt = rs.getTimestamp("access_token_issued_at").toInstant();
|
||||
tokenExpiresAt = rs.getTimestamp("access_token_expires_at").toInstant();
|
||||
Map<String, Object> accessTokenMetadata = parseMap(getLobValue(rs, "access_token_metadata"));
|
||||
OAuth2AccessToken.TokenType tokenType = null;
|
||||
if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(rs.getString("access_token_type"))) {
|
||||
tokenType = OAuth2AccessToken.TokenType.BEARER;
|
||||
}
|
||||
|
||||
Set<String> scopes = Collections.emptySet();
|
||||
String accessTokenScopes = rs.getString("access_token_scopes");
|
||||
if (accessTokenScopes != null) {
|
||||
scopes = StringUtils.commaDelimitedListToSet(accessTokenScopes);
|
||||
}
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, accessTokenValue, tokenIssuedAt, tokenExpiresAt, scopes);
|
||||
builder.token(accessToken, (metadata) -> metadata.putAll(accessTokenMetadata));
|
||||
}
|
||||
|
||||
String oidcIdTokenValue = getLobValue(rs, "oidc_id_token_value");
|
||||
if (StringUtils.hasText(oidcIdTokenValue)) {
|
||||
tokenIssuedAt = rs.getTimestamp("oidc_id_token_issued_at").toInstant();
|
||||
tokenExpiresAt = rs.getTimestamp("oidc_id_token_expires_at").toInstant();
|
||||
Map<String, Object> oidcTokenMetadata = parseMap(getLobValue(rs, "oidc_id_token_metadata"));
|
||||
|
||||
OidcIdToken oidcToken = new OidcIdToken(
|
||||
oidcIdTokenValue, tokenIssuedAt, tokenExpiresAt, (Map<String, Object>) oidcTokenMetadata.get(OAuth2Authorization.Token.CLAIMS_METADATA_NAME));
|
||||
builder.token(oidcToken, (metadata) -> metadata.putAll(oidcTokenMetadata));
|
||||
}
|
||||
|
||||
String refreshTokenValue = getLobValue(rs, "refresh_token_value");
|
||||
if (StringUtils.hasText(refreshTokenValue)) {
|
||||
tokenIssuedAt = rs.getTimestamp("refresh_token_issued_at").toInstant();
|
||||
tokenExpiresAt = null;
|
||||
Timestamp refreshTokenExpiresAt = rs.getTimestamp("refresh_token_expires_at");
|
||||
if (refreshTokenExpiresAt != null) {
|
||||
tokenExpiresAt = refreshTokenExpiresAt.toInstant();
|
||||
}
|
||||
Map<String, Object> refreshTokenMetadata = parseMap(getLobValue(rs, "refresh_token_metadata"));
|
||||
|
||||
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(
|
||||
refreshTokenValue, tokenIssuedAt, tokenExpiresAt);
|
||||
builder.token(refreshToken, (metadata) -> metadata.putAll(refreshTokenMetadata));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private String getLobValue(ResultSet rs, String columnName) throws SQLException {
|
||||
String columnValue = null;
|
||||
ColumnMetadata columnMetadata = columnMetadataMap.get(columnName);
|
||||
if (Types.BLOB == columnMetadata.getDataType()) {
|
||||
byte[] columnValueBytes = this.lobHandler.getBlobAsBytes(rs, columnName);
|
||||
if (columnValueBytes != null) {
|
||||
columnValue = new String(columnValueBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
} else if (Types.CLOB == columnMetadata.getDataType()) {
|
||||
columnValue = this.lobHandler.getClobAsString(rs, columnName);
|
||||
} else {
|
||||
columnValue = rs.getString(columnName);
|
||||
}
|
||||
return columnValue;
|
||||
}
|
||||
|
||||
public final void setLobHandler(LobHandler lobHandler) {
|
||||
Assert.notNull(lobHandler, "lobHandler cannot be null");
|
||||
this.lobHandler = lobHandler;
|
||||
}
|
||||
|
||||
public final void setObjectMapper(ObjectMapper objectMapper) {
|
||||
Assert.notNull(objectMapper, "objectMapper cannot be null");
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
protected final RegisteredClientRepository getRegisteredClientRepository() {
|
||||
return this.registeredClientRepository;
|
||||
}
|
||||
|
||||
protected final LobHandler getLobHandler() {
|
||||
return this.lobHandler;
|
||||
}
|
||||
|
||||
protected final ObjectMapper getObjectMapper() {
|
||||
return this.objectMapper;
|
||||
}
|
||||
|
||||
private Map<String, Object> parseMap(String data) {
|
||||
try {
|
||||
return this.objectMapper.readValue(data, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The default {@code Function} that maps {@link OAuth2Authorization} to a
|
||||
* {@code List} of {@link SqlParameterValue}.
|
||||
*/
|
||||
public static class OAuth2AuthorizationParametersMapper implements Function<OAuth2Authorization, List<SqlParameterValue>> {
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public OAuth2AuthorizationParametersMapper() {
|
||||
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
|
||||
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
|
||||
this.objectMapper.registerModules(securityModules);
|
||||
this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SqlParameterValue> apply(OAuth2Authorization authorization) {
|
||||
List<SqlParameterValue> parameters = new ArrayList<>();
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getId()));
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getRegisteredClientId()));
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getPrincipalName()));
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getAuthorizationGrantType().getValue()));
|
||||
|
||||
String attributes = writeMap(authorization.getAttributes());
|
||||
parameters.add(mapToSqlParameter("attributes", attributes));
|
||||
|
||||
String state = null;
|
||||
String authorizationState = authorization.getAttribute(OAuth2ParameterNames.STATE);
|
||||
if (StringUtils.hasText(authorizationState)) {
|
||||
state = authorizationState;
|
||||
}
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, state));
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
|
||||
authorization.getToken(OAuth2AuthorizationCode.class);
|
||||
List<SqlParameterValue> authorizationCodeSqlParameters = toSqlParameterList(
|
||||
"authorization_code_value", "authorization_code_metadata", authorizationCode);
|
||||
parameters.addAll(authorizationCodeSqlParameters);
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> accessToken =
|
||||
authorization.getToken(OAuth2AccessToken.class);
|
||||
List<SqlParameterValue> accessTokenSqlParameters = toSqlParameterList(
|
||||
"access_token_value", "access_token_metadata", accessToken);
|
||||
parameters.addAll(accessTokenSqlParameters);
|
||||
String accessTokenType = null;
|
||||
String accessTokenScopes = null;
|
||||
if (accessToken != null) {
|
||||
accessTokenType = accessToken.getToken().getTokenType().getValue();
|
||||
if (!CollectionUtils.isEmpty(accessToken.getToken().getScopes())) {
|
||||
accessTokenScopes = StringUtils.collectionToDelimitedString(accessToken.getToken().getScopes(), ",");
|
||||
}
|
||||
}
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, accessTokenType));
|
||||
parameters.add(new SqlParameterValue(Types.VARCHAR, accessTokenScopes));
|
||||
|
||||
OAuth2Authorization.Token<OidcIdToken> oidcIdToken = authorization.getToken(OidcIdToken.class);
|
||||
List<SqlParameterValue> oidcIdTokenSqlParameters = toSqlParameterList(
|
||||
"oidc_id_token_value", "oidc_id_token_metadata", oidcIdToken);
|
||||
parameters.addAll(oidcIdTokenSqlParameters);
|
||||
|
||||
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = authorization.getRefreshToken();
|
||||
List<SqlParameterValue> refreshTokenSqlParameters = toSqlParameterList(
|
||||
"refresh_token_value", "refresh_token_metadata", refreshToken);
|
||||
parameters.addAll(refreshTokenSqlParameters);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public final void setObjectMapper(ObjectMapper objectMapper) {
|
||||
Assert.notNull(objectMapper, "objectMapper cannot be null");
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
protected final ObjectMapper getObjectMapper() {
|
||||
return this.objectMapper;
|
||||
}
|
||||
|
||||
private <T extends AbstractOAuth2Token> List<SqlParameterValue> toSqlParameterList(
|
||||
String tokenColumnName, String tokenMetadataColumnName, OAuth2Authorization.Token<T> token) {
|
||||
|
||||
List<SqlParameterValue> parameters = new ArrayList<>();
|
||||
String tokenValue = null;
|
||||
Timestamp tokenIssuedAt = null;
|
||||
Timestamp tokenExpiresAt = null;
|
||||
String metadata = null;
|
||||
if (token != null) {
|
||||
tokenValue = token.getToken().getTokenValue();
|
||||
if (token.getToken().getIssuedAt() != null) {
|
||||
tokenIssuedAt = Timestamp.from(token.getToken().getIssuedAt());
|
||||
}
|
||||
if (token.getToken().getExpiresAt() != null) {
|
||||
tokenExpiresAt = Timestamp.from(token.getToken().getExpiresAt());
|
||||
}
|
||||
metadata = writeMap(token.getMetadata());
|
||||
}
|
||||
|
||||
parameters.add(mapToSqlParameter(tokenColumnName, tokenValue));
|
||||
parameters.add(new SqlParameterValue(Types.TIMESTAMP, tokenIssuedAt));
|
||||
parameters.add(new SqlParameterValue(Types.TIMESTAMP, tokenExpiresAt));
|
||||
parameters.add(mapToSqlParameter(tokenMetadataColumnName, metadata));
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private String writeMap(Map<String, Object> data) {
|
||||
try {
|
||||
return this.objectMapper.writeValueAsString(data);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class LobCreatorArgumentPreparedStatementSetter extends ArgumentPreparedStatementSetter {
|
||||
private final LobCreator lobCreator;
|
||||
|
||||
private LobCreatorArgumentPreparedStatementSetter(LobCreator lobCreator, Object[] args) {
|
||||
super(args);
|
||||
this.lobCreator = lobCreator;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSetValue(PreparedStatement ps, int parameterPosition, Object argValue) throws SQLException {
|
||||
if (argValue instanceof SqlParameterValue) {
|
||||
SqlParameterValue paramValue = (SqlParameterValue) argValue;
|
||||
if (paramValue.getSqlType() == Types.BLOB) {
|
||||
if (paramValue.getValue() != null) {
|
||||
Assert.isInstanceOf(byte[].class, paramValue.getValue(),
|
||||
"Value of blob parameter must be byte[]");
|
||||
}
|
||||
byte[] valueBytes = (byte[]) paramValue.getValue();
|
||||
this.lobCreator.setBlobAsBytes(ps, parameterPosition, valueBytes);
|
||||
return;
|
||||
}
|
||||
if (paramValue.getSqlType() == Types.CLOB) {
|
||||
if (paramValue.getValue() != null) {
|
||||
Assert.isInstanceOf(String.class, paramValue.getValue(),
|
||||
"Value of clob parameter must be String");
|
||||
}
|
||||
String valueString = (String) paramValue.getValue();
|
||||
this.lobCreator.setClobAsString(ps, parameterPosition, valueString);
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.doSetValue(ps, parameterPosition, argValue);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class ColumnMetadata {
|
||||
private final String columnName;
|
||||
private final int dataType;
|
||||
|
||||
private ColumnMetadata(String columnName, int dataType) {
|
||||
this.columnName = columnName;
|
||||
this.dataType = dataType;
|
||||
}
|
||||
|
||||
private String getColumnName() {
|
||||
return this.columnName;
|
||||
}
|
||||
|
||||
private int getDataType() {
|
||||
return this.dataType;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void initColumnMetadata(JdbcOperations jdbcOperations) {
|
||||
columnMetadataMap = new HashMap<>();
|
||||
ColumnMetadata columnMetadata;
|
||||
|
||||
columnMetadata = getColumnMetadata(jdbcOperations, "attributes", Types.BLOB);
|
||||
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
|
||||
columnMetadata = getColumnMetadata(jdbcOperations, "authorization_code_value", Types.BLOB);
|
||||
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
|
||||
columnMetadata = getColumnMetadata(jdbcOperations, "authorization_code_metadata", Types.BLOB);
|
||||
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
|
||||
columnMetadata = getColumnMetadata(jdbcOperations, "access_token_value", Types.BLOB);
|
||||
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
|
||||
columnMetadata = getColumnMetadata(jdbcOperations, "access_token_metadata", Types.BLOB);
|
||||
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
|
||||
columnMetadata = getColumnMetadata(jdbcOperations, "oidc_id_token_value", Types.BLOB);
|
||||
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
|
||||
columnMetadata = getColumnMetadata(jdbcOperations, "oidc_id_token_metadata", Types.BLOB);
|
||||
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
|
||||
columnMetadata = getColumnMetadata(jdbcOperations, "refresh_token_value", Types.BLOB);
|
||||
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
|
||||
columnMetadata = getColumnMetadata(jdbcOperations, "refresh_token_metadata", Types.BLOB);
|
||||
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
|
||||
}
|
||||
|
||||
private static ColumnMetadata getColumnMetadata(JdbcOperations jdbcOperations, String columnName, int defaultDataType) {
|
||||
Integer dataType = jdbcOperations.execute((ConnectionCallback<Integer>) conn -> {
|
||||
DatabaseMetaData databaseMetaData = conn.getMetaData();
|
||||
ResultSet rs = databaseMetaData.getColumns(null, null, TABLE_NAME, columnName);
|
||||
if (rs.next()) {
|
||||
return rs.getInt("DATA_TYPE");
|
||||
}
|
||||
// NOTE: (Applies to HSQL)
|
||||
// When a database object is created with one of the CREATE statements or renamed with the ALTER statement,
|
||||
// if the name is enclosed in double quotes, the exact name is used as the case-normal form.
|
||||
// But if it is not enclosed in double quotes,
|
||||
// the name is converted to uppercase and this uppercase version is stored in the database as the case-normal form.
|
||||
rs = databaseMetaData.getColumns(null, null, TABLE_NAME.toUpperCase(), columnName.toUpperCase());
|
||||
if (rs.next()) {
|
||||
return rs.getInt("DATA_TYPE");
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return new ColumnMetadata(columnName, dataType != null ? dataType : defaultDataType);
|
||||
}
|
||||
|
||||
private static SqlParameterValue mapToSqlParameter(String columnName, String value) {
|
||||
ColumnMetadata columnMetadata = columnMetadataMap.get(columnName);
|
||||
return Types.BLOB == columnMetadata.getDataType() && StringUtils.hasText(value) ?
|
||||
new SqlParameterValue(Types.BLOB, value.getBytes(StandardCharsets.UTF_8)) :
|
||||
new SqlParameterValue(columnMetadata.getDataType(), value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,52 +15,82 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.core.context.Context;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2TokenContext} implementation used when encoding a {@link Jwt}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see OAuth2TokenContext
|
||||
* @see JoseHeader.Builder
|
||||
* @see JwtClaimsSet.Builder
|
||||
* @see JwtEncoder#encode(JoseHeader, JwtClaimsSet)
|
||||
*/
|
||||
public final class JwtEncodingContext implements OAuth2TokenContext {
|
||||
private final Context context;
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
private JwtEncodingContext(Map<Object, Object> context) {
|
||||
this.context = Context.of(context);
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return this.context.get(key);
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
return this.context.hasKey(key);
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JoseHeader.Builder headers}
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @return the {@link JoseHeader.Builder}
|
||||
*/
|
||||
public JoseHeader.Builder getHeaders() {
|
||||
return get(JoseHeader.Builder.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JwtClaimsSet.Builder claims}
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @return the {@link JwtClaimsSet.Builder}
|
||||
*/
|
||||
public JwtClaimsSet.Builder getClaims() {
|
||||
return get(JwtClaimsSet.Builder.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided headers and claims.
|
||||
*
|
||||
* @param headersBuilder the headers to initialize the builder
|
||||
* @param claimsBuilder the claims to initialize the builder
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(JoseHeader.Builder headersBuilder, JwtClaimsSet.Builder claimsBuilder) {
|
||||
return new Builder(headersBuilder, claimsBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link JwtEncodingContext}.
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<JwtEncodingContext, Builder> {
|
||||
|
||||
private Builder(JoseHeader.Builder headersBuilder, JwtClaimsSet.Builder claimsBuilder) {
|
||||
@@ -70,18 +100,43 @@ public final class JwtEncodingContext implements OAuth2TokenContext {
|
||||
put(JwtClaimsSet.Builder.class, claimsBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the {@link JoseHeader.Builder headers}
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @deprecated Use {@link #getHeaders()} instead
|
||||
* @param headersConsumer a {@code Consumer} of the {@link JoseHeader.Builder headers}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
@Deprecated
|
||||
public Builder headers(Consumer<JoseHeader.Builder> headersConsumer) {
|
||||
headersConsumer.accept(get(JoseHeader.Builder.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the {@link JwtClaimsSet.Builder claims}
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @deprecated Use {@link #getClaims()} instead
|
||||
* @param claimsConsumer a {@code Consumer} of the {@link JwtClaimsSet.Builder claims}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
@Deprecated
|
||||
public Builder claims(Consumer<JwtClaimsSet.Builder> claimsConsumer) {
|
||||
claimsConsumer.accept(get(JwtClaimsSet.Builder.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link JwtEncodingContext}.
|
||||
*
|
||||
* @return the {@link JwtEncodingContext}
|
||||
*/
|
||||
public JwtEncodingContext build() {
|
||||
return new JwtEncodingContext(this.context);
|
||||
return new JwtEncodingContext(getContext());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,12 +25,11 @@ import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -46,7 +45,7 @@ import org.springframework.util.StringUtils;
|
||||
* @since 0.0.1
|
||||
* @see RegisteredClient
|
||||
* @see AuthorizationGrantType
|
||||
* @see AbstractOAuth2Token
|
||||
* @see OAuth2Token
|
||||
* @see OAuth2AccessToken
|
||||
* @see OAuth2RefreshToken
|
||||
*/
|
||||
@@ -64,7 +63,7 @@ public class OAuth2Authorization implements Serializable {
|
||||
private String registeredClientId;
|
||||
private String principalName;
|
||||
private AuthorizationGrantType authorizationGrantType;
|
||||
private Map<Class<? extends AbstractOAuth2Token>, Token<?>> tokens;
|
||||
private Map<Class<? extends OAuth2Token>, Token<?>> tokens;
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
protected OAuth2Authorization() {
|
||||
@@ -134,7 +133,7 @@ public class OAuth2Authorization implements Serializable {
|
||||
*/
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends AbstractOAuth2Token> Token<T> getToken(Class<T> tokenType) {
|
||||
public <T extends OAuth2Token> Token<T> getToken(Class<T> tokenType) {
|
||||
Assert.notNull(tokenType, "tokenType cannot be null");
|
||||
Token<?> token = this.tokens.get(tokenType);
|
||||
return token != null ? (Token<T>) token : null;
|
||||
@@ -149,13 +148,14 @@ public class OAuth2Authorization implements Serializable {
|
||||
*/
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends AbstractOAuth2Token> Token<T> getToken(String tokenValue) {
|
||||
public <T extends OAuth2Token> Token<T> getToken(String tokenValue) {
|
||||
Assert.hasText(tokenValue, "tokenValue cannot be empty");
|
||||
Token<?> token = this.tokens.values().stream()
|
||||
.filter(t -> t.getToken().getTokenValue().equals(tokenValue))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
return token != null ? (Token<T>) token : null;
|
||||
for (Token<?> token : this.tokens.values()) {
|
||||
if (token.getToken().getTokenValue().equals(tokenValue)) {
|
||||
return (Token<T>) token;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,19 +237,19 @@ public class OAuth2Authorization implements Serializable {
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
*/
|
||||
public static class Token<T extends AbstractOAuth2Token> implements Serializable {
|
||||
public static class Token<T extends OAuth2Token> implements Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
protected static final String TOKEN_METADATA_BASE = "metadata.token.";
|
||||
protected static final String TOKEN_METADATA_NAMESPACE = "metadata.token.";
|
||||
|
||||
/**
|
||||
* The name of the metadata that indicates if the token has been invalidated.
|
||||
*/
|
||||
public static final String INVALIDATED_METADATA_NAME = TOKEN_METADATA_BASE.concat("invalidated");
|
||||
public static final String INVALIDATED_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("invalidated");
|
||||
|
||||
/**
|
||||
* The name of the metadata used for the claims of the token.
|
||||
*/
|
||||
public static final String CLAIMS_METADATA_NAME = TOKEN_METADATA_BASE.concat("claims");
|
||||
public static final String CLAIMS_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("claims");
|
||||
|
||||
private final T token;
|
||||
private final Map<String, Object> metadata;
|
||||
@@ -264,9 +264,9 @@ public class OAuth2Authorization implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the token of type {@link AbstractOAuth2Token}.
|
||||
* Returns the token of type {@link OAuth2Token}.
|
||||
*
|
||||
* @return the token of type {@link AbstractOAuth2Token}
|
||||
* @return the token of type {@link OAuth2Token}
|
||||
*/
|
||||
public T getToken() {
|
||||
return this.token;
|
||||
@@ -380,7 +380,7 @@ public class OAuth2Authorization implements Serializable {
|
||||
private final String registeredClientId;
|
||||
private String principalName;
|
||||
private AuthorizationGrantType authorizationGrantType;
|
||||
private Map<Class<? extends AbstractOAuth2Token>, Token<?>> tokens = new HashMap<>();
|
||||
private Map<Class<? extends OAuth2Token>, Token<?>> tokens = new HashMap<>();
|
||||
private final Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
protected Builder(String registeredClientId) {
|
||||
@@ -441,25 +441,25 @@ public class OAuth2Authorization implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AbstractOAuth2Token token}.
|
||||
* Sets the {@link OAuth2Token token}.
|
||||
*
|
||||
* @param token the token
|
||||
* @param <T> the type of the token
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public <T extends AbstractOAuth2Token> Builder token(T token) {
|
||||
public <T extends OAuth2Token> Builder token(T token) {
|
||||
return token(token, (metadata) -> {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AbstractOAuth2Token token} and associated metadata.
|
||||
* Sets the {@link OAuth2Token token} and associated metadata.
|
||||
*
|
||||
* @param token the token
|
||||
* @param metadataConsumer a {@code Consumer} of the metadata {@code Map}
|
||||
* @param <T> the type of the token
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public <T extends AbstractOAuth2Token> Builder token(T token,
|
||||
public <T extends OAuth2Token> Builder token(T token,
|
||||
Consumer<Map<String, Object>> metadataConsumer) {
|
||||
|
||||
Assert.notNull(token, "token cannot be null");
|
||||
@@ -469,15 +469,12 @@ public class OAuth2Authorization implements Serializable {
|
||||
metadata.putAll(existingToken.getMetadata());
|
||||
}
|
||||
metadataConsumer.accept(metadata);
|
||||
Class<? extends AbstractOAuth2Token> tokenClass = token.getClass();
|
||||
if (tokenClass.equals(OAuth2RefreshToken2.class)) {
|
||||
tokenClass = OAuth2RefreshToken.class;
|
||||
}
|
||||
Class<? extends OAuth2Token> tokenClass = token.getClass();
|
||||
this.tokens.put(tokenClass, new Token<>(token, metadata));
|
||||
return this;
|
||||
}
|
||||
|
||||
protected final Builder tokens(Map<Class<? extends AbstractOAuth2Token>, Token<?>> tokens) {
|
||||
protected final Builder tokens(Map<Class<? extends OAuth2Token>, Token<?>> tokens) {
|
||||
this.tokens = new HashMap<>(tokens);
|
||||
return this;
|
||||
}
|
||||
@@ -529,5 +526,7 @@ public class OAuth2Authorization implements Serializable {
|
||||
authorization.attributes = Collections.unmodifiableMap(this.attributes);
|
||||
return authorization;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A representation of an OAuth 2.0 "consent" to an Authorization request, which holds state related to the
|
||||
* set of {@link #getAuthorities() authorities} granted to a {@link #getRegisteredClientId() client} by the
|
||||
* {@link #getPrincipalName() resource owner}.
|
||||
* <p>
|
||||
* When authorizing access for a given client, the resource owner may only grant a subset of the authorities
|
||||
* the client requested. The typical use-case is the {@code authorization_code} flow, in which the client
|
||||
* requests a set of {@code scope}s. The resource owner then selects which scopes they grant to the client.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.1.2
|
||||
*/
|
||||
public final class OAuth2AuthorizationConsent implements Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private static final String AUTHORITIES_SCOPE_PREFIX = "SCOPE_";
|
||||
|
||||
private final String registeredClientId;
|
||||
private final String principalName;
|
||||
private final Set<GrantedAuthority> authorities;
|
||||
|
||||
private OAuth2AuthorizationConsent(String registeredClientId, String principalName, Set<GrantedAuthority> authorities) {
|
||||
this.registeredClientId = registeredClientId;
|
||||
this.principalName = principalName;
|
||||
this.authorities = Collections.unmodifiableSet(authorities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier for the {@link RegisteredClient#getId() registered client}.
|
||||
*
|
||||
* @return the {@link RegisteredClient#getId()}
|
||||
*/
|
||||
public String getRegisteredClientId() {
|
||||
return this.registeredClientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code Principal} name of the resource owner (or client).
|
||||
*
|
||||
* @return the {@code Principal} name of the resource owner (or client)
|
||||
*/
|
||||
public String getPrincipalName() {
|
||||
return this.principalName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link GrantedAuthority authorities} granted to the client by the principal.
|
||||
*
|
||||
* @return the {@link GrantedAuthority authorities} granted to the client by the principal.
|
||||
*/
|
||||
public Set<GrantedAuthority> getAuthorities() {
|
||||
return this.authorities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for obtaining the {@code scope}s granted to the client by the principal,
|
||||
* extracted from the {@link #getAuthorities() authorities}.
|
||||
*
|
||||
* @return the {@code scope}s granted to the client by the principal.
|
||||
*/
|
||||
public Set<String> getScopes() {
|
||||
Set<String> authorities = new HashSet<>();
|
||||
for (GrantedAuthority authority : getAuthorities()) {
|
||||
if (authority.getAuthority().startsWith(AUTHORITIES_SCOPE_PREFIX)) {
|
||||
authorities.add(authority.getAuthority().replaceFirst(AUTHORITIES_SCOPE_PREFIX, ""));
|
||||
}
|
||||
}
|
||||
return authorities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
OAuth2AuthorizationConsent that = (OAuth2AuthorizationConsent) obj;
|
||||
return Objects.equals(this.registeredClientId, that.registeredClientId) &&
|
||||
Objects.equals(this.principalName, that.principalName) &&
|
||||
Objects.equals(this.authorities, that.authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.registeredClientId, this.principalName, this.authorities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the values from the provided {@code OAuth2AuthorizationConsent}.
|
||||
*
|
||||
* @param authorizationConsent the {@code OAuth2AuthorizationConsent} used for initializing the {@link Builder}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder from(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||
return new Builder(
|
||||
authorizationConsent.getRegisteredClientId(),
|
||||
authorizationConsent.getPrincipalName(),
|
||||
authorizationConsent.getAuthorities()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the given {@link RegisteredClient#getClientId() registeredClientId}
|
||||
* and {@code Principal} name.
|
||||
*
|
||||
* @param registeredClientId the {@link RegisteredClient#getId()}
|
||||
* @param principalName the {@code Principal} name
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withId(@NonNull String registeredClientId, @NonNull String principalName) {
|
||||
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
|
||||
Assert.hasText(principalName, "principalName cannot be empty");
|
||||
return new Builder(registeredClientId, principalName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A builder for {@link OAuth2AuthorizationConsent}.
|
||||
*/
|
||||
public static final class Builder implements Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
|
||||
private final String registeredClientId;
|
||||
private final String principalName;
|
||||
private final Set<GrantedAuthority> authorities = new HashSet<>();
|
||||
|
||||
private Builder(String registeredClientId, String principalName) {
|
||||
this(registeredClientId, principalName, Collections.emptySet());
|
||||
}
|
||||
|
||||
private Builder(String registeredClientId, String principalName, Set<GrantedAuthority> authorities) {
|
||||
this.registeredClientId = registeredClientId;
|
||||
this.principalName = principalName;
|
||||
if (!CollectionUtils.isEmpty(authorities)) {
|
||||
this.authorities.addAll(authorities);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a scope to the collection of {@code authorities} in the resulting {@link OAuth2AuthorizationConsent},
|
||||
* wrapping it in a {@link SimpleGrantedAuthority}, prefixed by {@code SCOPE_}. For example, a
|
||||
* {@code message.write} scope would be stored as {@code SCOPE_message.write}.
|
||||
*
|
||||
* @param scope the scope
|
||||
* @return the {@code Builder} for further configuration
|
||||
*/
|
||||
public Builder scope(String scope) {
|
||||
authority(new SimpleGrantedAuthority(AUTHORITIES_SCOPE_PREFIX + scope));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link GrantedAuthority} to the collection of {@code authorities} in the
|
||||
* resulting {@link OAuth2AuthorizationConsent}.
|
||||
*
|
||||
* @param authority the {@link GrantedAuthority}
|
||||
* @return the {@code Builder} for further configuration
|
||||
*/
|
||||
public Builder authority(GrantedAuthority authority) {
|
||||
this.authorities.add(authority);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the {@code authorities}, allowing the ability to add, replace or remove.
|
||||
*
|
||||
* @param authoritiesConsumer a {@code Consumer} of the {@code authorities}
|
||||
* @return the {@code Builder} for further configuration
|
||||
*/
|
||||
public Builder authorities(Consumer<Set<GrantedAuthority>> authoritiesConsumer) {
|
||||
authoritiesConsumer.accept(this.authorities);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the authorities and build the {@link OAuth2AuthorizationConsent}.
|
||||
* There must be at least one {@link GrantedAuthority}.
|
||||
*
|
||||
* @return the {@link OAuth2AuthorizationConsent}
|
||||
*/
|
||||
public OAuth2AuthorizationConsent build() {
|
||||
Assert.notEmpty(this.authorities, "authorities cannot be empty");
|
||||
return new OAuth2AuthorizationConsent(this.registeredClientId, this.principalName, this.authorities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for the management
|
||||
* of {@link OAuth2AuthorizationConsent OAuth 2.0 Authorization Consent(s)}.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationConsent
|
||||
*/
|
||||
public interface OAuth2AuthorizationConsentService {
|
||||
|
||||
/**
|
||||
* Saves the {@link OAuth2AuthorizationConsent}.
|
||||
*
|
||||
* @param authorizationConsent the {@link OAuth2AuthorizationConsent}
|
||||
*/
|
||||
void save(OAuth2AuthorizationConsent authorizationConsent);
|
||||
|
||||
/**
|
||||
* Removes the {@link OAuth2AuthorizationConsent}.
|
||||
*
|
||||
* @param authorizationConsent the {@link OAuth2AuthorizationConsent}
|
||||
*/
|
||||
void remove(OAuth2AuthorizationConsent authorizationConsent);
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2AuthorizationConsent} identified by the provided
|
||||
* {@code registeredClientId} and {@code principalName}, or {@code null} if not found.
|
||||
*
|
||||
* @param registeredClientId the identifier for the {@link RegisteredClient}
|
||||
* @param principalName the name of the {@link Principal}
|
||||
* @return the {@link OAuth2AuthorizationConsent} if found, otherwise {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
OAuth2AuthorizationConsent findById(String registeredClientId, String principalName);
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,81 +27,201 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.context.Context;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A context that holds information (to be) associated to an OAuth 2.0 Token
|
||||
* and is used by an {@link OAuth2TokenGenerator} and {@link OAuth2TokenCustomizer}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see Context
|
||||
* @see OAuth2TokenGenerator
|
||||
* @see OAuth2TokenCustomizer
|
||||
*/
|
||||
public interface OAuth2TokenContext extends Context {
|
||||
|
||||
/**
|
||||
* Returns the {@link RegisteredClient registered client}.
|
||||
*
|
||||
* @return the {@link RegisteredClient}
|
||||
*/
|
||||
default RegisteredClient getRegisteredClient() {
|
||||
return get(RegisteredClient.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Authentication} representing the {@code Principal} resource owner (or client).
|
||||
*
|
||||
* @param <T> the type of the {@code Authentication}
|
||||
* @return the {@link Authentication} representing the {@code Principal} resource owner (or client)
|
||||
*/
|
||||
default <T extends Authentication> T getPrincipal() {
|
||||
return get(AbstractBuilder.PRINCIPAL_AUTHENTICATION_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ProviderContext provider context}.
|
||||
*
|
||||
* @return the {@link ProviderContext}
|
||||
* @since 0.2.3
|
||||
*/
|
||||
default ProviderContext getProviderContext() {
|
||||
return get(ProviderContext.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2Authorization authorization}.
|
||||
*
|
||||
* @return the {@link OAuth2Authorization}, or {@code null} if not available
|
||||
*/
|
||||
@Nullable
|
||||
default OAuth2Authorization getAuthorization() {
|
||||
return get(OAuth2Authorization.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorized scope(s).
|
||||
*
|
||||
* @return the authorized scope(s)
|
||||
*/
|
||||
default Set<String> getAuthorizedScopes() {
|
||||
return hasKey(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME) ?
|
||||
get(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME) :
|
||||
Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2TokenType token type}.
|
||||
*
|
||||
* @return the {@link OAuth2TokenType}
|
||||
*/
|
||||
default OAuth2TokenType getTokenType() {
|
||||
return get(OAuth2TokenType.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link AuthorizationGrantType authorization grant type}.
|
||||
*
|
||||
* @return the {@link AuthorizationGrantType}
|
||||
*/
|
||||
default AuthorizationGrantType getAuthorizationGrantType() {
|
||||
return get(AuthorizationGrantType.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Authentication} representing the authorization grant.
|
||||
*
|
||||
* @param <T> the type of the {@code Authentication}
|
||||
* @return the {@link Authentication} representing the authorization grant
|
||||
*/
|
||||
default <T extends Authentication> T getAuthorizationGrant() {
|
||||
return get(AbstractBuilder.AUTHORIZATION_GRANT_AUTHENTICATION_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base builder for implementations of {@link OAuth2TokenContext}.
|
||||
*
|
||||
* @param <T> the type of the context
|
||||
* @param <B> the type of the builder
|
||||
*/
|
||||
abstract class AbstractBuilder<T extends OAuth2TokenContext, B extends AbstractBuilder<T, B>> {
|
||||
private static final String PRINCIPAL_AUTHENTICATION_KEY =
|
||||
Authentication.class.getName().concat(".PRINCIPAL");
|
||||
private static final String AUTHORIZATION_GRANT_AUTHENTICATION_KEY =
|
||||
Authentication.class.getName().concat(".AUTHORIZATION_GRANT");
|
||||
protected final Map<Object, Object> context = new HashMap<>();
|
||||
private final Map<Object, Object> context = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Sets the {@link RegisteredClient registered client}.
|
||||
*
|
||||
* @param registeredClient the {@link RegisteredClient}
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B registeredClient(RegisteredClient registeredClient) {
|
||||
return put(RegisteredClient.class, registeredClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Authentication} representing the {@code Principal} resource owner (or client).
|
||||
*
|
||||
* @param principal the {@link Authentication} representing the {@code Principal} resource owner (or client)
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B principal(Authentication principal) {
|
||||
return put(PRINCIPAL_AUTHENTICATION_KEY, principal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ProviderContext provider context}.
|
||||
*
|
||||
* @param providerContext the {@link ProviderContext}
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public B providerContext(ProviderContext providerContext) {
|
||||
return put(ProviderContext.class, providerContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2Authorization authorization}.
|
||||
*
|
||||
* @param authorization the {@link OAuth2Authorization}
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B authorization(OAuth2Authorization authorization) {
|
||||
return put(OAuth2Authorization.class, authorization);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authorized scope(s).
|
||||
*
|
||||
* @param authorizedScopes the authorized scope(s)
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B authorizedScopes(Set<String> authorizedScopes) {
|
||||
return put(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenType token type}.
|
||||
*
|
||||
* @param tokenType the {@link OAuth2TokenType}
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B tokenType(OAuth2TokenType tokenType) {
|
||||
return put(OAuth2TokenType.class, tokenType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthorizationGrantType authorization grant type}.
|
||||
*
|
||||
* @param authorizationGrantType the {@link AuthorizationGrantType}
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B authorizationGrantType(AuthorizationGrantType authorizationGrantType) {
|
||||
return put(AuthorizationGrantType.class, authorizationGrantType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Authentication} representing the authorization grant.
|
||||
*
|
||||
* @param authorizationGrant the {@link Authentication} representing the authorization grant
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B authorizationGrant(Authentication authorizationGrant) {
|
||||
return put(AUTHORIZATION_GRANT_AUTHENTICATION_KEY, authorizationGrant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates an attribute.
|
||||
*
|
||||
* @param key the key for the attribute
|
||||
* @param value the value of the attribute
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B put(Object key, Object value) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
@@ -109,6 +229,13 @@ public interface OAuth2TokenContext extends Context {
|
||||
return getThis();
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the attributes {@code Map}
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param contextConsumer a {@link Consumer} of the attributes {@code Map}
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B context(Consumer<Map<Object, Object>> contextConsumer) {
|
||||
contextConsumer.accept(this.context);
|
||||
return getThis();
|
||||
@@ -119,12 +246,22 @@ public interface OAuth2TokenContext extends Context {
|
||||
return (V) this.context.get(key);
|
||||
}
|
||||
|
||||
protected Map<Object, Object> getContext() {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected B getThis() {
|
||||
protected final B getThis() {
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OAuth2TokenContext}.
|
||||
*
|
||||
* @return the {@link OAuth2TokenContext}
|
||||
*/
|
||||
public abstract T build();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,13 +16,22 @@
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for customizing the
|
||||
* OAuth 2.0 Token attributes contained within the {@link OAuth2TokenContext}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see OAuth2TokenContext
|
||||
* @param <T> the type of the context containing the OAuth 2.0 Token attributes
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface OAuth2TokenCustomizer<C extends OAuth2TokenContext> {
|
||||
public interface OAuth2TokenCustomizer<T extends OAuth2TokenContext> {
|
||||
|
||||
void customize(C context);
|
||||
/**
|
||||
* Customize the OAuth 2.0 Token attributes.
|
||||
*
|
||||
* @param context the context containing the OAuth 2.0 Token attributes
|
||||
*/
|
||||
void customize(T context);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation used for OAuth 2.0 Client Authentication,
|
||||
* which authenticates the {@link OAuth2ParameterNames#CLIENT_SECRET client_secret} parameter.
|
||||
*
|
||||
* @author Patryk Kostrzewa
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.3
|
||||
* @see AuthenticationProvider
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see PasswordEncoder
|
||||
*/
|
||||
public final class ClientSecretAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final CodeVerifierAuthenticator codeVerifierAuthenticator;
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
/**
|
||||
* Constructs a {@code ClientSecretAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param registeredClientRepository the repository of registered clients
|
||||
* @param authorizationService the authorization service
|
||||
*/
|
||||
public ClientSecretAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
|
||||
OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
|
||||
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link PasswordEncoder} used to validate
|
||||
* the {@link RegisteredClient#getClientSecret() client secret}.
|
||||
* If not set, the client secret will be compared using
|
||||
* {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}.
|
||||
*
|
||||
* @param passwordEncoder the {@link PasswordEncoder} used to validate the client secret
|
||||
*/
|
||||
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
|
||||
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2ClientAuthenticationToken clientAuthentication =
|
||||
(OAuth2ClientAuthenticationToken) authentication;
|
||||
|
||||
if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) &&
|
||||
!ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String clientId = clientAuthentication.getPrincipal().toString();
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
|
||||
if (registeredClient == null) {
|
||||
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
|
||||
}
|
||||
|
||||
if (!registeredClient.getClientAuthenticationMethods().contains(
|
||||
clientAuthentication.getClientAuthenticationMethod())) {
|
||||
throwInvalidClient("authentication_method");
|
||||
}
|
||||
|
||||
if (clientAuthentication.getCredentials() == null) {
|
||||
throwInvalidClient("credentials");
|
||||
}
|
||||
|
||||
String clientSecret = clientAuthentication.getCredentials().toString();
|
||||
if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
|
||||
throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
|
||||
}
|
||||
|
||||
// Validate the "code_verifier" parameter for the confidential client, if available
|
||||
this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(registeredClient,
|
||||
clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private static void throwInvalidClient(String parameterName) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Client authentication failed: " + parameterName,
|
||||
ERROR_URI
|
||||
);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
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.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An authenticator used for OAuth 2.0 Client Authentication,
|
||||
* which authenticates the {@link PkceParameterNames#CODE_VERIFIER code_verifier} parameter.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.3
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see OAuth2AuthorizationService
|
||||
*/
|
||||
final class CodeVerifierAuthenticator {
|
||||
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
CodeVerifierAuthenticator(OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
void authenticateRequired(OAuth2ClientAuthenticationToken clientAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
if (!authenticate(clientAuthentication, registeredClient)) {
|
||||
throwInvalidGrant(PkceParameterNames.CODE_VERIFIER);
|
||||
}
|
||||
}
|
||||
|
||||
void authenticateIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
authenticate(clientAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
private boolean authenticate(OAuth2ClientAuthenticationToken clientAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
|
||||
Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
|
||||
if (!authorizationCodeGrant(parameters)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
(String) parameters.get(OAuth2ParameterNames.CODE),
|
||||
AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
throwInvalidGrant(OAuth2ParameterNames.CODE);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
||||
OAuth2AuthorizationRequest.class.getName());
|
||||
|
||||
String codeChallenge = (String) authorizationRequest.getAdditionalParameters()
|
||||
.get(PkceParameterNames.CODE_CHALLENGE);
|
||||
if (!StringUtils.hasText(codeChallenge)) {
|
||||
if (registeredClient.getClientSettings().isRequireProofKey()) {
|
||||
throwInvalidGrant(PkceParameterNames.CODE_CHALLENGE);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String codeChallengeMethod = (String) authorizationRequest.getAdditionalParameters()
|
||||
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
|
||||
String codeVerifier = (String) parameters.get(PkceParameterNames.CODE_VERIFIER);
|
||||
if (!codeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) {
|
||||
throwInvalidGrant(PkceParameterNames.CODE_VERIFIER);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean authorizationCodeGrant(Map<String, Object> parameters) {
|
||||
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
|
||||
parameters.get(OAuth2ParameterNames.GRANT_TYPE)) &&
|
||||
parameters.get(OAuth2ParameterNames.CODE) != null;
|
||||
}
|
||||
|
||||
private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
|
||||
if (!StringUtils.hasText(codeVerifier)) {
|
||||
return false;
|
||||
} else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) {
|
||||
return codeVerifier.equals(codeChallenge);
|
||||
} else if ("S256".equals(codeChallengeMethod)) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
|
||||
String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
return encodedVerifier.equals(codeChallenge);
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
// It is unlikely that SHA-256 is not available on the server. If it is not available,
|
||||
// there will likely be bigger issues as well. We default to SERVER_ERROR.
|
||||
}
|
||||
}
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.SERVER_ERROR);
|
||||
}
|
||||
|
||||
private static void throwInvalidGrant(String parameterName) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_GRANT,
|
||||
"Client authentication failed: " + parameterName,
|
||||
null
|
||||
);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
|
||||
import org.springframework.security.oauth2.jwt.JwtException;
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation used for OAuth 2.0 Client Authentication,
|
||||
* which authenticates the (JWT) {@link OAuth2ParameterNames#CLIENT_ASSERTION client_assertion} parameter.
|
||||
*
|
||||
* @author Rafal Lewczuk
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.3
|
||||
* @see AuthenticationProvider
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
*/
|
||||
public final class JwtClientAssertionAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
|
||||
private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD =
|
||||
new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final CodeVerifierAuthenticator codeVerifierAuthenticator;
|
||||
private final JwtClientAssertionDecoderFactory jwtClientAssertionDecoderFactory;
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwtClientAssertionAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param registeredClientRepository the repository of registered clients
|
||||
* @param authorizationService the authorization service
|
||||
*/
|
||||
public JwtClientAssertionAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
|
||||
OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
|
||||
this.jwtClientAssertionDecoderFactory = new JwtClientAssertionDecoderFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2ClientAuthenticationToken clientAuthentication =
|
||||
(OAuth2ClientAuthenticationToken) authentication;
|
||||
|
||||
if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String clientId = clientAuthentication.getPrincipal().toString();
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
|
||||
if (registeredClient == null) {
|
||||
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
|
||||
}
|
||||
|
||||
if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) &&
|
||||
!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
|
||||
throwInvalidClient("authentication_method");
|
||||
}
|
||||
|
||||
if (clientAuthentication.getCredentials() == null) {
|
||||
throwInvalidClient("credentials");
|
||||
}
|
||||
|
||||
Jwt jwtAssertion = null;
|
||||
JwtDecoder jwtDecoder = this.jwtClientAssertionDecoderFactory.createDecoder(registeredClient);
|
||||
try {
|
||||
jwtAssertion = jwtDecoder.decode(clientAuthentication.getCredentials().toString());
|
||||
} catch (JwtException ex) {
|
||||
throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION, ex);
|
||||
}
|
||||
|
||||
// Validate the "code_verifier" parameter for the confidential client, if available
|
||||
this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
|
||||
|
||||
ClientAuthenticationMethod clientAuthenticationMethod =
|
||||
registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm() instanceof SignatureAlgorithm ?
|
||||
ClientAuthenticationMethod.PRIVATE_KEY_JWT :
|
||||
ClientAuthenticationMethod.CLIENT_SECRET_JWT;
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(registeredClient, clientAuthenticationMethod, jwtAssertion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private static void throwInvalidClient(String parameterName) {
|
||||
throwInvalidClient(parameterName, null);
|
||||
}
|
||||
|
||||
private static void throwInvalidClient(String parameterName, Throwable cause) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Client authentication failed: " + parameterName,
|
||||
ERROR_URI
|
||||
);
|
||||
throw new OAuth2AuthenticationException(error, error.toString(), cause);
|
||||
}
|
||||
|
||||
private static class JwtClientAssertionDecoderFactory implements JwtDecoderFactory<RegisteredClient> {
|
||||
private static final String JWT_CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7523#section-3";
|
||||
|
||||
private static final Map<JwsAlgorithm, String> JCA_ALGORITHM_MAPPINGS;
|
||||
|
||||
static {
|
||||
Map<JwsAlgorithm, String> mappings = new HashMap<>();
|
||||
mappings.put(MacAlgorithm.HS256, "HmacSHA256");
|
||||
mappings.put(MacAlgorithm.HS384, "HmacSHA384");
|
||||
mappings.put(MacAlgorithm.HS512, "HmacSHA512");
|
||||
JCA_ALGORITHM_MAPPINGS = Collections.unmodifiableMap(mappings);
|
||||
}
|
||||
|
||||
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public JwtDecoder createDecoder(RegisteredClient registeredClient) {
|
||||
Assert.notNull(registeredClient, "registeredClient cannot be null");
|
||||
return this.jwtDecoders.computeIfAbsent(registeredClient.getId(), (key) -> {
|
||||
NimbusJwtDecoder jwtDecoder = buildDecoder(registeredClient);
|
||||
jwtDecoder.setJwtValidator(createJwtValidator(registeredClient));
|
||||
return jwtDecoder;
|
||||
});
|
||||
}
|
||||
|
||||
private static NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {
|
||||
JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();
|
||||
if (jwsAlgorithm instanceof SignatureAlgorithm) {
|
||||
String jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();
|
||||
if (!StringUtils.hasText(jwkSetUrl)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Failed to find a Signature Verifier for Client: '"
|
||||
+ registeredClient.getId()
|
||||
+ "'. Check to ensure you have configured the JWK Set URL.",
|
||||
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error);
|
||||
}
|
||||
return NimbusJwtDecoder.withJwkSetUri(jwkSetUrl).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();
|
||||
}
|
||||
if (jwsAlgorithm instanceof MacAlgorithm) {
|
||||
String clientSecret = registeredClient.getClientSecret();
|
||||
if (!StringUtils.hasText(clientSecret)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Failed to find a Signature Verifier for Client: '"
|
||||
+ registeredClient.getId()
|
||||
+ "'. Check to ensure you have configured the client secret.",
|
||||
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error);
|
||||
}
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
|
||||
JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
|
||||
return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
|
||||
}
|
||||
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Failed to find a Signature Verifier for Client: '"
|
||||
+ registeredClient.getId()
|
||||
+ "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'.",
|
||||
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error);
|
||||
}
|
||||
|
||||
private static OAuth2TokenValidator<Jwt> createJwtValidator(RegisteredClient registeredClient) {
|
||||
String clientId = registeredClient.getClientId();
|
||||
return new DelegatingOAuth2TokenValidator<>(
|
||||
new JwtClaimValidator<>(JwtClaimNames.ISS, clientId::equals),
|
||||
new JwtClaimValidator<>(JwtClaimNames.SUB, clientId::equals),
|
||||
new JwtClaimValidator<>(JwtClaimNames.AUD, containsProviderAudience()),
|
||||
new JwtClaimValidator<>(JwtClaimNames.EXP, Objects::nonNull),
|
||||
new JwtTimestampValidator()
|
||||
);
|
||||
}
|
||||
|
||||
private static Predicate<List<String>> containsProviderAudience() {
|
||||
return (audienceClaim) -> {
|
||||
if (CollectionUtils.isEmpty(audienceClaim)) {
|
||||
return false;
|
||||
}
|
||||
List<String> providerAudience = getProviderAudience();
|
||||
for (String audience : audienceClaim) {
|
||||
if (providerAudience.contains(audience)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
private static List<String> getProviderAudience() {
|
||||
ProviderContext providerContext = ProviderContextHolder.getProviderContext();
|
||||
if (!StringUtils.hasText(providerContext.getIssuer())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
ProviderSettings providerSettings = providerContext.getProviderSettings();
|
||||
List<String> providerAudience = new ArrayList<>();
|
||||
providerAudience.add(providerContext.getIssuer());
|
||||
providerAudience.add(asUrl(providerContext.getIssuer(), providerSettings.getTokenEndpoint()));
|
||||
providerAudience.add(asUrl(providerContext.getIssuer(), providerSettings.getTokenIntrospectionEndpoint()));
|
||||
providerAudience.add(asUrl(providerContext.getIssuer(), providerSettings.getTokenRevocationEndpoint()));
|
||||
return providerAudience;
|
||||
}
|
||||
|
||||
private static String asUrl(String issuer, String endpoint) {
|
||||
return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Utility methods used by the {@link AuthenticationProvider}'s when issuing {@link Jwt}'s.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class JwtUtils {
|
||||
|
||||
private JwtUtils() {
|
||||
}
|
||||
|
||||
static JoseHeader.Builder headers() {
|
||||
return JoseHeader.withAlgorithm(SignatureAlgorithm.RS256);
|
||||
}
|
||||
|
||||
static JwtClaimsSet.Builder accessTokenClaims(RegisteredClient registeredClient,
|
||||
String issuer, String subject, Set<String> authorizedScopes) {
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().accessTokenTimeToLive());
|
||||
|
||||
// @formatter:off
|
||||
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();
|
||||
if (StringUtils.hasText(issuer)) {
|
||||
claimsBuilder.issuer(issuer);
|
||||
}
|
||||
claimsBuilder
|
||||
.subject(subject)
|
||||
.audience(Collections.singletonList(registeredClient.getClientId()))
|
||||
.issuedAt(issuedAt)
|
||||
.expiresAt(expiresAt)
|
||||
.notBefore(issuedAt);
|
||||
if (!CollectionUtils.isEmpty(authorizedScopes)) {
|
||||
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, authorizedScopes);
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
return claimsBuilder;
|
||||
}
|
||||
|
||||
static JwtClaimsSet.Builder idTokenClaims(RegisteredClient registeredClient,
|
||||
String issuer, String subject, String nonce) {
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
// TODO Allow configuration for ID Token time-to-live
|
||||
Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES);
|
||||
|
||||
// @formatter:off
|
||||
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();
|
||||
if (StringUtils.hasText(issuer)) {
|
||||
claimsBuilder.issuer(issuer);
|
||||
}
|
||||
claimsBuilder
|
||||
.subject(subject)
|
||||
.audience(Collections.singletonList(registeredClient.getClientId()))
|
||||
.issuedAt(issuedAt)
|
||||
.expiresAt(expiresAt)
|
||||
.claim(IdTokenClaimNames.AZP, registeredClient.getClientId());
|
||||
if (StringUtils.hasText(nonce)) {
|
||||
claimsBuilder.claim(IdTokenClaimNames.NONCE, nonce);
|
||||
}
|
||||
// TODO Add 'auth_time' claim
|
||||
// @formatter:on
|
||||
|
||||
return claimsBuilder;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,11 +19,10 @@ import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
|
||||
/**
|
||||
* Utility methods for the OAuth 2.0 {@link AuthenticationProvider}'s.
|
||||
@@ -44,7 +43,7 @@ final class OAuth2AuthenticationProviderUtils {
|
||||
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||
return clientPrincipal;
|
||||
}
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
static <T extends AbstractOAuth2Token> OAuth2Authorization invalidate(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,38 +16,48 @@
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -61,44 +71,89 @@ import static org.springframework.security.oauth2.server.authorization.authentic
|
||||
* @since 0.0.1
|
||||
* @see OAuth2AuthorizationCodeAuthenticationToken
|
||||
* @see OAuth2AccessTokenAuthenticationToken
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see JwtEncoder
|
||||
* @see OAuth2TokenCustomizer
|
||||
* @see JwtEncodingContext
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
|
||||
* @see OAuth2TokenGenerator
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
|
||||
*/
|
||||
public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
public final class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE =
|
||||
new OAuth2TokenType(OAuth2ParameterNames.CODE);
|
||||
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE =
|
||||
new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final JwtEncoder jwtEncoder;
|
||||
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
|
||||
private ProviderSettings providerSettings;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
|
||||
// TODO Remove after removing @Deprecated OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService, JwtEncoder)
|
||||
private JwtGenerator jwtGenerator;
|
||||
|
||||
@Deprecated
|
||||
private Supplier<String> refreshTokenGenerator;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @deprecated Use {@link #OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService, OAuth2TokenGenerator)} instead
|
||||
* @param authorizationService the authorization service
|
||||
* @param jwtEncoder the jwt encoder
|
||||
*/
|
||||
@Deprecated
|
||||
public OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.jwtGenerator = new JwtGenerator(jwtEncoder);
|
||||
this.tokenGenerator = new DelegatingOAuth2TokenGenerator(this.jwtGenerator,
|
||||
new OAuth2AccessTokenGenerator(), new OAuth2RefreshTokenGenerator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param authorizationService the authorization service
|
||||
* @param jwtEncoder the jwt encoder
|
||||
* @param tokenGenerator the token generator
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) {
|
||||
public OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService authorizationService,
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.jwtEncoder = jwtEncoder;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
}
|
||||
|
||||
public final void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenCustomizer} that customizes the
|
||||
* {@link JwtEncodingContext.Builder#headers(Consumer) headers} and/or
|
||||
* {@link JwtEncodingContext.Builder#claims(Consumer) claims} for the generated {@link Jwt}.
|
||||
*
|
||||
* @deprecated Use {@link JwtGenerator#setJwtCustomizer(OAuth2TokenCustomizer)} instead
|
||||
* @param jwtCustomizer the {@link OAuth2TokenCustomizer} that customizes the headers and/or claims for the generated {@code Jwt}
|
||||
*/
|
||||
@Deprecated
|
||||
public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
|
||||
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
|
||||
this.jwtCustomizer = jwtCustomizer;
|
||||
if (this.jwtGenerator != null) {
|
||||
this.jwtGenerator.setJwtCustomizer(jwtCustomizer);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
/**
|
||||
* Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}.
|
||||
*
|
||||
* @deprecated Use {@link OAuth2RefreshTokenGenerator} instead
|
||||
* @param refreshTokenGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}
|
||||
*/
|
||||
@Deprecated
|
||||
public void setRefreshTokenGenerator(Supplier<String> refreshTokenGenerator) {
|
||||
Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null");
|
||||
this.refreshTokenGenerator = refreshTokenGenerator;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
protected void setProviderSettings(ProviderSettings providerSettings) {
|
||||
this.providerSettings = providerSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -113,7 +168,7 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
authorizationCodeAuthentication.getCode(), AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
|
||||
authorization.getToken(OAuth2AuthorizationCode.class);
|
||||
@@ -127,107 +182,91 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
||||
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken());
|
||||
this.authorizationService.save(authorization);
|
||||
}
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(authorizationRequest.getRedirectUri()) &&
|
||||
!authorizationRequest.getRedirectUri().equals(authorizationCodeAuthentication.getRedirectUri())) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
if (authorizationCode.isInvalidated()) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
if (!authorizationCode.isActive()) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null;
|
||||
Set<String> authorizedScopes = authorization.getAttribute(
|
||||
OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
|
||||
|
||||
JoseHeader.Builder headersBuilder = JwtUtils.headers();
|
||||
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(
|
||||
registeredClient, issuer, authorization.getPrincipalName(),
|
||||
authorizedScopes);
|
||||
|
||||
// @formatter:off
|
||||
JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(authorization.getAttribute(Principal.class.getName()))
|
||||
.providerContext(ProviderContextHolder.getProviderContext())
|
||||
.authorization(authorization)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
|
||||
.authorizedScopes(authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrant(authorizationCodeAuthentication)
|
||||
.build();
|
||||
.authorizationGrant(authorizationCodeAuthentication);
|
||||
// @formatter:on
|
||||
|
||||
this.jwtCustomizer.customize(context);
|
||||
|
||||
JoseHeader headers = context.getHeaders().build();
|
||||
JwtClaimsSet claims = context.getClaims().build();
|
||||
Jwt jwtAccessToken = this.jwtEncoder.encode(headers, claims);
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
|
||||
|
||||
// ----- Access token -----
|
||||
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (generatedAccessToken == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the access token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
|
||||
jwtAccessToken.getExpiresAt(), authorizedScopes);
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||
authorizationBuilder.token(accessToken, (metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
|
||||
} else {
|
||||
authorizationBuilder.accessToken(accessToken);
|
||||
}
|
||||
|
||||
// ----- Refresh token -----
|
||||
OAuth2RefreshToken refreshToken = null;
|
||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
|
||||
refreshToken = OAuth2RefreshTokenAuthenticationProvider.generateRefreshToken(
|
||||
registeredClient.getTokenSettings().refreshTokenTimeToLive());
|
||||
}
|
||||
|
||||
Jwt jwtIdToken = null;
|
||||
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) {
|
||||
String nonce = (String) authorizationRequest.getAdditionalParameters().get(OidcParameterNames.NONCE);
|
||||
|
||||
headersBuilder = JwtUtils.headers();
|
||||
claimsBuilder = JwtUtils.idTokenClaims(
|
||||
registeredClient, issuer, authorization.getPrincipalName(), nonce);
|
||||
|
||||
// @formatter:off
|
||||
context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
|
||||
.registeredClient(registeredClient)
|
||||
.principal(authorization.getAttribute(Principal.class.getName()))
|
||||
.authorization(authorization)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.tokenType(ID_TOKEN_TOKEN_TYPE)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrant(authorizationCodeAuthentication)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
this.jwtCustomizer.customize(context);
|
||||
|
||||
headers = context.getHeaders().build();
|
||||
claims = context.getClaims().build();
|
||||
jwtIdToken = this.jwtEncoder.encode(headers, claims);
|
||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||
// Do not issue refresh token to public client
|
||||
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||
|
||||
if (this.refreshTokenGenerator != null) {
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive());
|
||||
refreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt);
|
||||
} else {
|
||||
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the refresh token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||
}
|
||||
authorizationBuilder.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
// ----- ID token -----
|
||||
OidcIdToken idToken;
|
||||
if (jwtIdToken != null) {
|
||||
idToken = new OidcIdToken(jwtIdToken.getTokenValue(), jwtIdToken.getIssuedAt(),
|
||||
jwtIdToken.getExpiresAt(), jwtIdToken.getClaims());
|
||||
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) {
|
||||
tokenContext = tokenContextBuilder.tokenType(ID_TOKEN_TOKEN_TYPE).build();
|
||||
OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (!(generatedIdToken instanceof Jwt)) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the ID token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
|
||||
generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
|
||||
authorizationBuilder.token(idToken, (metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
|
||||
} else {
|
||||
idToken = null;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization)
|
||||
.token(accessToken,
|
||||
(metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, jwtAccessToken.getClaims())
|
||||
);
|
||||
if (refreshToken != null) {
|
||||
authorizationBuilder.refreshToken(refreshToken);
|
||||
}
|
||||
if (idToken != null) {
|
||||
authorizationBuilder
|
||||
.token(idToken,
|
||||
(metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
|
||||
}
|
||||
authorization = authorizationBuilder.build();
|
||||
// @formatter:on
|
||||
|
||||
// Invalidate the authorization code as it can only be used once
|
||||
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken());
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
|
||||
/**
|
||||
* This exception is thrown by {@link OAuth2AuthorizationCodeRequestAuthenticationProvider}
|
||||
* when an attempt to authenticate the OAuth 2.0 Authorization Request (or Consent) fails.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
*/
|
||||
public class OAuth2AuthorizationCodeRequestAuthenticationException extends OAuth2AuthenticationException {
|
||||
private final OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationException} using the provided parameters.
|
||||
*
|
||||
* @param error the {@link OAuth2Error OAuth 2.0 Error}
|
||||
* @param authorizationCodeRequestAuthentication the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent)
|
||||
*/
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationException(OAuth2Error error,
|
||||
@Nullable OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
|
||||
super(error);
|
||||
this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationException} using the provided parameters.
|
||||
*
|
||||
* @param error the {@link OAuth2Error OAuth 2.0 Error}
|
||||
* @param cause the root cause
|
||||
* @param authorizationCodeRequestAuthentication the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent)
|
||||
*/
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationException(OAuth2Error error, Throwable cause,
|
||||
@Nullable OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
|
||||
super(error, cause);
|
||||
this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent), or {@code null} if not available.
|
||||
*
|
||||
* @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
*/
|
||||
@Nullable
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationToken getAuthorizationCodeRequestAuthentication() {
|
||||
return this.authorizationCodeRequestAuthentication;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,706 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationContext;
|
||||
import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationValidator;
|
||||
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.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent)
|
||||
* used in the Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
|
||||
* @see OAuth2AuthorizationCodeAuthenticationProvider
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2AuthorizationConsentService
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
|
||||
*/
|
||||
public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
|
||||
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
|
||||
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
|
||||
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
|
||||
new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
||||
private static final Function<String, OAuth2AuthenticationValidator> DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER =
|
||||
createDefaultAuthenticationValidatorResolver();
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2AuthorizationConsentService authorizationConsentService;
|
||||
|
||||
@Deprecated
|
||||
private Supplier<String> authorizationCodeSupplier;
|
||||
|
||||
private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
|
||||
private Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER;
|
||||
private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param registeredClientRepository the repository of registered clients
|
||||
* @param authorizationService the authorization service
|
||||
* @param authorizationConsentService the authorization consent service
|
||||
*/
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
|
||||
OAuth2AuthorizationService authorizationService, OAuth2AuthorizationConsentService authorizationConsentService) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.authorizationService = authorizationService;
|
||||
this.authorizationConsentService = authorizationConsentService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
||||
|
||||
return authorizationCodeRequestAuthentication.isConsent() ?
|
||||
authenticateAuthorizationConsent(authentication) :
|
||||
authenticateAuthorizationRequest(authentication);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @deprecated Use {@link #setAuthorizationCodeGenerator(OAuth2TokenGenerator)} instead
|
||||
* @param authorizationCodeGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}
|
||||
*/
|
||||
@Deprecated
|
||||
public void setAuthorizationCodeGenerator(Supplier<String> authorizationCodeGenerator) {
|
||||
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
|
||||
this.authorizationCodeSupplier = authorizationCodeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
|
||||
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
|
||||
this.authorizationCodeGenerator = authorizationCodeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter.
|
||||
*
|
||||
* <p>
|
||||
* The following OAuth 2.0 Authorization Request parameters are supported:
|
||||
* <ol>
|
||||
* <li>{@link OAuth2ParameterNames#REDIRECT_URI}</li>
|
||||
* <li>{@link OAuth2ParameterNames#SCOPE}</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The resolved {@link OAuth2AuthenticationValidator} MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
|
||||
*
|
||||
* @param authenticationValidatorResolver the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter
|
||||
*/
|
||||
public void setAuthenticationValidatorResolver(Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver) {
|
||||
Assert.notNull(authenticationValidatorResolver, "authenticationValidatorResolver cannot be null");
|
||||
this.authenticationValidatorResolver = authenticationValidatorResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationConsentAuthenticationContext}
|
||||
* containing an {@link OAuth2AuthorizationConsent.Builder} and additional context information.
|
||||
*
|
||||
* <p>
|
||||
* The following context attributes are available:
|
||||
* <ul>
|
||||
* <li>The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization consent
|
||||
* prior to {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.</li>
|
||||
* <li>The {@link Authentication} of type
|
||||
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.</li>
|
||||
* <li>The {@link RegisteredClient} associated with the authorization request.</li>
|
||||
* <li>The {@link OAuth2Authorization} associated with the state token presented in the
|
||||
* authorization consent request.</li>
|
||||
* <li>The {@link OAuth2AuthorizationRequest} associated with the authorization consent request.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param authorizationConsentCustomizer the {@code Consumer} providing access to the
|
||||
* {@link OAuth2AuthorizationConsentAuthenticationContext} containing an {@link OAuth2AuthorizationConsent.Builder}
|
||||
*/
|
||||
public void setAuthorizationConsentCustomizer(Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer) {
|
||||
Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null");
|
||||
this.authorizationConsentCustomizer = authorizationConsentCustomizer;
|
||||
}
|
||||
|
||||
private Authentication authenticateAuthorizationRequest(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
authorizationCodeRequestAuthentication.getClientId());
|
||||
if (registeredClient == null) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationCodeRequestAuthentication, null);
|
||||
}
|
||||
|
||||
Map<Object, Object> context = new HashMap<>();
|
||||
context.put(RegisteredClient.class, registeredClient);
|
||||
OAuth2AuthenticationContext authenticationContext = new OAuth2AuthenticationContext(
|
||||
authorizationCodeRequestAuthentication, context);
|
||||
|
||||
OAuth2AuthenticationValidator redirectUriValidator = resolveAuthenticationValidator(OAuth2ParameterNames.REDIRECT_URI);
|
||||
redirectUriValidator.validate(authenticationContext);
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
|
||||
throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
OAuth2AuthenticationValidator scopeValidator = resolveAuthenticationValidator(OAuth2ParameterNames.SCOPE);
|
||||
scopeValidator.validate(authenticationContext);
|
||||
|
||||
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
|
||||
String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);
|
||||
if (StringUtils.hasText(codeChallenge)) {
|
||||
String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD);
|
||||
if (StringUtils.hasText(codeChallengeMethod)) {
|
||||
if (!"S256".equals(codeChallengeMethod) && !"plain".equals(codeChallengeMethod)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient, null);
|
||||
}
|
||||
}
|
||||
} else if (registeredClient.getClientSettings().isRequireProofKey()) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient, null);
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// The request is valid - ensure the resource owner is authenticated
|
||||
// ---------------
|
||||
|
||||
Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
|
||||
if (!isPrincipalAuthenticated(principal)) {
|
||||
// Return the authorization request as-is where isAuthenticated() is false
|
||||
return authorizationCodeRequestAuthentication;
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
|
||||
.clientId(registeredClient.getClientId())
|
||||
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
|
||||
.scopes(authorizationCodeRequestAuthentication.getScopes())
|
||||
.state(authorizationCodeRequestAuthentication.getState())
|
||||
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
|
||||
.build();
|
||||
|
||||
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
|
||||
registeredClient.getId(), principal.getName());
|
||||
|
||||
if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {
|
||||
String state = DEFAULT_STATE_GENERATOR.generateKey();
|
||||
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
|
||||
.attribute(OAuth2ParameterNames.STATE, state)
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
|
||||
currentAuthorizationConsent.getScopes() : null;
|
||||
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
|
||||
.authorizationUri(authorizationRequest.getAuthorizationUri())
|
||||
.scopes(currentAuthorizedScopes)
|
||||
.state(state)
|
||||
.consentRequired(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCode authorizationCode;
|
||||
if (this.authorizationCodeSupplier != null) {
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
|
||||
authorizationCode = new OAuth2AuthorizationCode(this.authorizationCodeSupplier.get(), issuedAt, expiresAt);
|
||||
} else {
|
||||
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
|
||||
authorizationCodeRequestAuthentication, registeredClient, null, authorizationRequest.getScopes());
|
||||
authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
|
||||
if (authorizationCode == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the authorization code.", ERROR_URI);
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
|
||||
}
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
|
||||
.token(authorizationCode)
|
||||
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes())
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
String redirectUri = authorizationRequest.getRedirectUri();
|
||||
if (!StringUtils.hasText(redirectUri)) {
|
||||
redirectUri = registeredClient.getRedirectUris().iterator().next();
|
||||
}
|
||||
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
|
||||
.authorizationUri(authorizationRequest.getAuthorizationUri())
|
||||
.redirectUri(redirectUri)
|
||||
.scopes(authorizationRequest.getScopes())
|
||||
.state(authorizationRequest.getState())
|
||||
.authorizationCode(authorizationCode)
|
||||
.build();
|
||||
}
|
||||
|
||||
private OAuth2AuthenticationValidator resolveAuthenticationValidator(String parameterName) {
|
||||
OAuth2AuthenticationValidator authenticationValidator = this.authenticationValidatorResolver.apply(parameterName);
|
||||
return authenticationValidator != null ?
|
||||
authenticationValidator :
|
||||
DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER.apply(parameterName);
|
||||
}
|
||||
|
||||
private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
authorizationCodeRequestAuthentication.getState(), STATE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
|
||||
authorizationCodeRequestAuthentication, null, null);
|
||||
}
|
||||
|
||||
// The 'in-flight' authorization must be associated to the current principal
|
||||
Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
|
||||
if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
|
||||
authorizationCodeRequestAuthentication, null, null);
|
||||
}
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
authorizationCodeRequestAuthentication.getClientId());
|
||||
if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
Set<String> requestedScopes = authorizationRequest.getScopes();
|
||||
Set<String> authorizedScopes = new HashSet<>(authorizationCodeRequestAuthentication.getScopes());
|
||||
if (!requestedScopes.containsAll(authorizedScopes)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName());
|
||||
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
|
||||
currentAuthorizationConsent.getScopes() : Collections.emptySet();
|
||||
|
||||
if (!currentAuthorizedScopes.isEmpty()) {
|
||||
for (String requestedScope : requestedScopes) {
|
||||
if (currentAuthorizedScopes.contains(requestedScope)) {
|
||||
authorizedScopes.add(requestedScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) {
|
||||
// 'openid' scope is auto-approved as it does not require consent
|
||||
authorizedScopes.add(OidcScopes.OPENID);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
|
||||
if (currentAuthorizationConsent != null) {
|
||||
authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
|
||||
} else {
|
||||
authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName());
|
||||
}
|
||||
authorizedScopes.forEach(authorizationConsentBuilder::scope);
|
||||
|
||||
if (this.authorizationConsentCustomizer != null) {
|
||||
// @formatter:off
|
||||
OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
|
||||
OAuth2AuthorizationConsentAuthenticationContext.with(authorizationCodeRequestAuthentication)
|
||||
.authorizationConsent(authorizationConsentBuilder)
|
||||
.registeredClient(registeredClient)
|
||||
.authorization(authorization)
|
||||
.authorizationRequest(authorizationRequest)
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
|
||||
}
|
||||
|
||||
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||
authorizationConsentBuilder.authorities(authorities::addAll);
|
||||
|
||||
if (authorities.isEmpty()) {
|
||||
// Authorization consent denied (or revoked)
|
||||
if (currentAuthorizationConsent != null) {
|
||||
this.authorizationConsentService.remove(currentAuthorizationConsent);
|
||||
}
|
||||
this.authorizationService.remove(authorization);
|
||||
throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
|
||||
if (!authorizationConsent.equals(currentAuthorizationConsent)) {
|
||||
this.authorizationConsentService.save(authorizationConsent);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCode authorizationCode;
|
||||
if (this.authorizationCodeSupplier != null) {
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
|
||||
authorizationCode = new OAuth2AuthorizationCode(this.authorizationCodeSupplier.get(), issuedAt, expiresAt);
|
||||
} else {
|
||||
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorization, authorizedScopes);
|
||||
authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
|
||||
if (authorizationCode == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the authorization code.", ERROR_URI);
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
|
||||
}
|
||||
}
|
||||
|
||||
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
|
||||
.token(authorizationCode)
|
||||
.attributes(attrs -> {
|
||||
attrs.remove(OAuth2ParameterNames.STATE);
|
||||
attrs.put(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
|
||||
})
|
||||
.build();
|
||||
this.authorizationService.save(updatedAuthorization);
|
||||
|
||||
String redirectUri = authorizationRequest.getRedirectUri();
|
||||
if (!StringUtils.hasText(redirectUri)) {
|
||||
redirectUri = registeredClient.getRedirectUris().iterator().next();
|
||||
}
|
||||
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
|
||||
.authorizationUri(authorizationRequest.getAuthorizationUri())
|
||||
.redirectUri(redirectUri)
|
||||
.scopes(authorizedScopes)
|
||||
.state(authorizationRequest.getState())
|
||||
.authorizationCode(authorizationCode)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static Function<String, OAuth2AuthenticationValidator> createDefaultAuthenticationValidatorResolver() {
|
||||
Map<String, OAuth2AuthenticationValidator> authenticationValidators = new HashMap<>();
|
||||
authenticationValidators.put(OAuth2ParameterNames.REDIRECT_URI, new DefaultRedirectUriOAuth2AuthenticationValidator());
|
||||
authenticationValidators.put(OAuth2ParameterNames.SCOPE, new DefaultScopeOAuth2AuthenticationValidator());
|
||||
return authenticationValidators::get;
|
||||
}
|
||||
|
||||
private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal,
|
||||
OAuth2AuthorizationRequest authorizationRequest) {
|
||||
return OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.principalName(principal.getName())
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.attribute(Principal.class.getName(), principal)
|
||||
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
|
||||
}
|
||||
|
||||
private static OAuth2TokenContext createAuthorizationCodeTokenContext(
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2Authorization authorization, Set<String> authorizedScopes) {
|
||||
|
||||
// @formatter:off
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal((Authentication) authorizationCodeRequestAuthentication.getPrincipal())
|
||||
.providerContext(ProviderContextHolder.getProviderContext())
|
||||
.tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE))
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrant(authorizationCodeRequestAuthentication);
|
||||
// @formatter:on
|
||||
|
||||
if (authorization != null) {
|
||||
tokenContextBuilder.authorization(authorization);
|
||||
}
|
||||
|
||||
return tokenContextBuilder.build();
|
||||
}
|
||||
|
||||
private static boolean requireAuthorizationConsent(RegisteredClient registeredClient,
|
||||
OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
|
||||
|
||||
if (!registeredClient.getClientSettings().isRequireAuthorizationConsent()) {
|
||||
return false;
|
||||
}
|
||||
// 'openid' scope does not require consent
|
||||
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID) &&
|
||||
authorizationRequest.getScopes().size() == 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (authorizationConsent != null &&
|
||||
authorizationConsent.getScopes().containsAll(authorizationRequest.getScopes())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isValidRedirectUri(String requestedRedirectUri, RegisteredClient registeredClient) {
|
||||
UriComponents requestedRedirect;
|
||||
try {
|
||||
requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
|
||||
if (requestedRedirect.getFragment() != null) {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String requestedRedirectHost = requestedRedirect.getHost();
|
||||
if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
|
||||
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7.1
|
||||
// While redirect URIs using localhost (i.e.,
|
||||
// "http://localhost:{port}/{path}") function similarly to loopback IP
|
||||
// redirects described in Section 10.3.3, the use of "localhost" is NOT RECOMMENDED.
|
||||
return false;
|
||||
}
|
||||
if (!isLoopbackAddress(requestedRedirectHost)) {
|
||||
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7
|
||||
// When comparing client redirect URIs against pre-registered URIs,
|
||||
// authorization servers MUST utilize exact string matching.
|
||||
return registeredClient.getRedirectUris().contains(requestedRedirectUri);
|
||||
}
|
||||
|
||||
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-10.3.3
|
||||
// The authorization server MUST allow any port to be specified at the
|
||||
// time of the request for loopback IP redirect URIs, to accommodate
|
||||
// clients that obtain an available ephemeral port from the operating
|
||||
// system at the time of the request.
|
||||
for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
|
||||
UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
|
||||
registeredRedirect.port(requestedRedirect.getPort());
|
||||
if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isLoopbackAddress(String host) {
|
||||
// IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1"
|
||||
if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) {
|
||||
return true;
|
||||
}
|
||||
// IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255
|
||||
String[] ipv4Octets = host.split("\\.");
|
||||
if (ipv4Octets.length != 4) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
int[] address = new int[ipv4Octets.length];
|
||||
for (int i=0; i < ipv4Octets.length; i++) {
|
||||
address[i] = Integer.parseInt(ipv4Octets[i]);
|
||||
}
|
||||
return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 &&
|
||||
address[2] <= 255 && address[3] >= 1 && address[3] <= 255;
|
||||
} catch (NumberFormatException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPrincipalAuthenticated(Authentication principal) {
|
||||
return principal != null &&
|
||||
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
|
||||
principal.isAuthenticated();
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
throwError(errorCode, parameterName, authorizationCodeRequestAuthentication, registeredClient, null);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||
throwError(errorCode, parameterName, ERROR_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName, String errorUri,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||
|
||||
boolean redirectOnError = true;
|
||||
if (errorCode.equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
|
||||
(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
|
||||
parameterName.equals(OAuth2ParameterNames.REDIRECT_URI) ||
|
||||
parameterName.equals(OAuth2ParameterNames.STATE))) {
|
||||
redirectOnError = false;
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication;
|
||||
|
||||
if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
|
||||
String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
|
||||
String state = authorizationCodeRequestAuthentication.isConsent() && authorizationRequest != null ?
|
||||
authorizationRequest.getState() : authorizationCodeRequestAuthentication.getState();
|
||||
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
|
||||
.redirectUri(redirectUri)
|
||||
.state(state)
|
||||
.build();
|
||||
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
|
||||
} else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
|
||||
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
|
||||
.redirectUri(null) // Prevent redirects
|
||||
.build();
|
||||
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
|
||||
}
|
||||
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
|
||||
}
|
||||
|
||||
private static String resolveRedirectUri(OAuth2AuthorizationRequest authorizationRequest, RegisteredClient registeredClient) {
|
||||
if (authorizationRequest != null && StringUtils.hasText(authorizationRequest.getRedirectUri())) {
|
||||
return authorizationRequest.getRedirectUri();
|
||||
}
|
||||
if (registeredClient != null) {
|
||||
return registeredClient.getRedirectUris().iterator().next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(authorizationCodeRequestAuthentication.getClientId(), (Authentication) authorizationCodeRequestAuthentication.getPrincipal())
|
||||
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
|
||||
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
|
||||
.scopes(authorizationCodeRequestAuthentication.getScopes())
|
||||
.state(authorizationCodeRequestAuthentication.getState())
|
||||
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
|
||||
.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
|
||||
}
|
||||
|
||||
private static class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator<OAuth2AuthorizationCode> {
|
||||
private final StringKeyGenerator authorizationCodeGenerator =
|
||||
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
|
||||
if (context.getTokenType() == null ||
|
||||
!OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) {
|
||||
return null;
|
||||
}
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
|
||||
return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
|
||||
|
||||
@Override
|
||||
public void validate(OAuth2AuthenticationContext authenticationContext) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
authenticationContext.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
|
||||
|
||||
if (StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
|
||||
if (!isValidRedirectUri(authorizationCodeRequestAuthentication.getRedirectUri(), registeredClient)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
} else if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
|
||||
registeredClient.getRedirectUris().size() != 1) {
|
||||
// redirect_uri is REQUIRED for OpenID Connect
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class DefaultScopeOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
|
||||
|
||||
@Override
|
||||
public void validate(OAuth2AuthenticationContext authenticationContext) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
authenticationContext.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
|
||||
|
||||
Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
|
||||
Set<String> allowedScopes = registeredClient.getScopes();
|
||||
if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation for the OAuth 2.0 Authorization Request (and Consent)
|
||||
* used in the Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
*/
|
||||
public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private String authorizationUri;
|
||||
private String clientId;
|
||||
private Authentication principal;
|
||||
private String redirectUri;
|
||||
private Set<String> scopes;
|
||||
private String state;
|
||||
private Map<String, Object> additionalParameters;
|
||||
private boolean consentRequired;
|
||||
private boolean consent;
|
||||
private OAuth2AuthorizationCode authorizationCode;
|
||||
|
||||
private OAuth2AuthorizationCodeRequestAuthenticationToken() {
|
||||
super(Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization URI.
|
||||
*
|
||||
* @return the authorization URI
|
||||
*/
|
||||
public String getAuthorizationUri() {
|
||||
return this.authorizationUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the client identifier.
|
||||
*
|
||||
* @return the client identifier
|
||||
*/
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the redirect uri.
|
||||
*
|
||||
* @return the redirect uri
|
||||
*/
|
||||
@Nullable
|
||||
public String getRedirectUri() {
|
||||
return this.redirectUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested (or authorized) scope(s).
|
||||
*
|
||||
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not available
|
||||
*/
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state.
|
||||
*
|
||||
* @return the state
|
||||
*/
|
||||
@Nullable
|
||||
public String getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional parameters.
|
||||
*
|
||||
* @return the additional parameters
|
||||
*/
|
||||
public Map<String, Object> getAdditionalParameters() {
|
||||
return this.additionalParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if authorization consent is required, {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if authorization consent is required, {@code false} otherwise
|
||||
*/
|
||||
public boolean isConsentRequired() {
|
||||
return this.consentRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this {@code Authentication} represents an authorization consent request,
|
||||
* {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
|
||||
*/
|
||||
public boolean isConsent() {
|
||||
return this.consent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @return the {@link OAuth2AuthorizationCode}
|
||||
*/
|
||||
@Nullable
|
||||
public OAuth2AuthorizationCode getAuthorizationCode() {
|
||||
return this.authorizationCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the given client identifier
|
||||
* and {@code Principal} (Resource Owner).
|
||||
*
|
||||
* @param clientId the client identifier
|
||||
* @param principal the {@code Principal} (Resource Owner)
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(@NonNull String clientId, @NonNull Authentication principal) {
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
return new Builder(clientId, principal);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
*/
|
||||
public static final class Builder implements Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private String authorizationUri;
|
||||
private String clientId;
|
||||
private Authentication principal;
|
||||
private String redirectUri;
|
||||
private Set<String> scopes;
|
||||
private String state;
|
||||
private Map<String, Object> additionalParameters;
|
||||
private boolean consentRequired;
|
||||
private boolean consent;
|
||||
private OAuth2AuthorizationCode authorizationCode;
|
||||
|
||||
private Builder(String clientId, Authentication principal) {
|
||||
this.clientId = clientId;
|
||||
this.principal = principal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authorization URI.
|
||||
*
|
||||
* @param authorizationUri the authorization URI
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder authorizationUri(String authorizationUri) {
|
||||
this.authorizationUri = authorizationUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the redirect uri.
|
||||
*
|
||||
* @param redirectUri the redirect uri
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder redirectUri(String redirectUri) {
|
||||
this.redirectUri = redirectUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the requested (or authorized) scope(s).
|
||||
*
|
||||
* @param scopes the requested (or authorized) scope(s)
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder scopes(Set<String> scopes) {
|
||||
if (scopes != null) {
|
||||
this.scopes = new HashSet<>(scopes);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state.
|
||||
*
|
||||
* @param state the state
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder state(String state) {
|
||||
this.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the additional parameters.
|
||||
*
|
||||
* @param additionalParameters the additional parameters
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder additionalParameters(Map<String, Object> additionalParameters) {
|
||||
if (additionalParameters != null) {
|
||||
this.additionalParameters = new HashMap<>(additionalParameters);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if authorization consent is required, {@code false} otherwise.
|
||||
*
|
||||
* @param consentRequired {@code true} if authorization consent is required, {@code false} otherwise
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder consentRequired(boolean consentRequired) {
|
||||
this.consentRequired = consentRequired;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise.
|
||||
*
|
||||
* @param consent {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder consent(boolean consent) {
|
||||
this.consent = consent;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @param authorizationCode the {@link OAuth2AuthorizationCode}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder authorizationCode(OAuth2AuthorizationCode authorizationCode) {
|
||||
this.authorizationCode = authorizationCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
*
|
||||
* @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
*/
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationToken build() {
|
||||
Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty");
|
||||
if (this.consent) {
|
||||
Assert.hasText(this.state, "state cannot be empty");
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationToken();
|
||||
|
||||
authentication.authorizationUri = this.authorizationUri;
|
||||
authentication.clientId = this.clientId;
|
||||
authentication.principal = this.principal;
|
||||
authentication.redirectUri = this.redirectUri;
|
||||
authentication.scopes = Collections.unmodifiableSet(
|
||||
!CollectionUtils.isEmpty(this.scopes) ?
|
||||
this.scopes :
|
||||
Collections.emptySet());
|
||||
authentication.state = this.state;
|
||||
authentication.additionalParameters = Collections.unmodifiableMap(
|
||||
!CollectionUtils.isEmpty(this.additionalParameters) ?
|
||||
this.additionalParameters :
|
||||
Collections.emptyMap());
|
||||
authentication.consentRequired = this.consentRequired;
|
||||
authentication.consent = this.consent;
|
||||
authentication.authorizationCode = this.authorizationCode;
|
||||
if (this.authorizationCode != null || this.consentRequired) {
|
||||
authentication.setAuthenticated(true);
|
||||
}
|
||||
|
||||
return authentication;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationContext;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2AuthenticationContext} that holds an {@link OAuth2AuthorizationConsent.Builder} and additional information
|
||||
* and is used when customizing the building of the {@link OAuth2AuthorizationConsent}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.1
|
||||
* @see OAuth2AuthenticationContext
|
||||
* @see OAuth2AuthorizationConsent
|
||||
*/
|
||||
public final class OAuth2AuthorizationConsentAuthenticationContext extends OAuth2AuthenticationContext {
|
||||
|
||||
private OAuth2AuthorizationConsentAuthenticationContext(Map<Object, Object> context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2AuthorizationConsent.Builder authorization consent builder}.
|
||||
*
|
||||
* @return the {@link OAuth2AuthorizationConsent.Builder}
|
||||
*/
|
||||
public OAuth2AuthorizationConsent.Builder getAuthorizationConsent() {
|
||||
return get(OAuth2AuthorizationConsent.Builder.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link RegisteredClient registered client}.
|
||||
*
|
||||
* @return the {@link RegisteredClient}
|
||||
*/
|
||||
public RegisteredClient getRegisteredClient() {
|
||||
return get(RegisteredClient.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2Authorization authorization}.
|
||||
*
|
||||
* @return the {@link OAuth2Authorization}
|
||||
*/
|
||||
public OAuth2Authorization getAuthorization() {
|
||||
return get(OAuth2Authorization.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2AuthorizationRequest authorization request}.
|
||||
*
|
||||
* @return the {@link OAuth2AuthorizationRequest}
|
||||
*/
|
||||
public OAuth2AuthorizationRequest getAuthorizationRequest() {
|
||||
return get(OAuth2AuthorizationRequest.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
*
|
||||
* @param authentication the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
|
||||
return new Builder(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OAuth2AuthorizationConsentAuthenticationContext}.
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<OAuth2AuthorizationConsentAuthenticationContext, Builder> {
|
||||
|
||||
private Builder(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
|
||||
super(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2AuthorizationConsent.Builder authorization consent builder}.
|
||||
*
|
||||
* @param authorizationConsent the {@link OAuth2AuthorizationConsent.Builder}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder authorizationConsent(OAuth2AuthorizationConsent.Builder authorizationConsent) {
|
||||
return put(OAuth2AuthorizationConsent.Builder.class, authorizationConsent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RegisteredClient registered client}.
|
||||
*
|
||||
* @param registeredClient the {@link RegisteredClient}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder registeredClient(RegisteredClient registeredClient) {
|
||||
return put(RegisteredClient.class, registeredClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2Authorization authorization}.
|
||||
*
|
||||
* @param authorization the {@link OAuth2Authorization}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder authorization(OAuth2Authorization authorization) {
|
||||
return put(OAuth2Authorization.class, authorization);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2AuthorizationRequest authorization request}.
|
||||
*
|
||||
* @param authorizationRequest the {@link OAuth2AuthorizationRequest}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder authorizationRequest(OAuth2AuthorizationRequest authorizationRequest) {
|
||||
return put(OAuth2AuthorizationRequest.class, authorizationRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OAuth2AuthorizationConsentAuthenticationContext}.
|
||||
*
|
||||
* @return the {@link OAuth2AuthorizationConsentAuthenticationContext}
|
||||
*/
|
||||
public OAuth2AuthorizationConsentAuthenticationContext build() {
|
||||
Assert.notNull(get(OAuth2AuthorizationConsent.Builder.class), "authorizationConsentBuilder cannot be null");
|
||||
Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
|
||||
Assert.notNull(get(OAuth2Authorization.class), "authorization cannot be null");
|
||||
Assert.notNull(get(OAuth2AuthorizationRequest.class), "authorizationRequest cannot be null");
|
||||
return new OAuth2AuthorizationConsentAuthenticationContext(getContext());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,32 +15,18 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
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.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation used for authenticating an OAuth 2.0 Client.
|
||||
@@ -48,18 +34,25 @@ import org.springframework.util.StringUtils;
|
||||
* @author Joe Grandja
|
||||
* @author Patryk Kostrzewa
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @author Rafal Lewczuk
|
||||
* @since 0.0.1
|
||||
* @see AuthenticationProvider
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see PasswordEncoder
|
||||
* @see JwtClientAssertionAuthenticationProvider
|
||||
* @see ClientSecretAuthenticationProvider
|
||||
* @see PublicClientAuthenticationProvider
|
||||
* @deprecated This implementation is decomposed into {@link JwtClientAssertionAuthenticationProvider},
|
||||
* {@link ClientSecretAuthenticationProvider} and {@link PublicClientAuthenticationProvider}.
|
||||
*/
|
||||
public class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@Deprecated
|
||||
public final class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD =
|
||||
new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
||||
private final JwtClientAssertionAuthenticationProvider jwtClientAssertionAuthenticationProvider;
|
||||
private final ClientSecretAuthenticationProvider clientSecretAuthenticationProvider;
|
||||
private final PublicClientAuthenticationProvider publicClientAuthenticationProvider;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientAuthenticationProvider} using the provided parameters.
|
||||
@@ -71,9 +64,12 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
|
||||
OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.authorizationService = authorizationService;
|
||||
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
this.jwtClientAssertionAuthenticationProvider = new JwtClientAssertionAuthenticationProvider(
|
||||
registeredClientRepository, authorizationService);
|
||||
this.clientSecretAuthenticationProvider = new ClientSecretAuthenticationProvider(
|
||||
registeredClientRepository, authorizationService);
|
||||
this.publicClientAuthenticationProvider = new PublicClientAuthenticationProvider(
|
||||
registeredClientRepository, authorizationService);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,9 +80,12 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
|
||||
*
|
||||
* @param passwordEncoder the {@link PasswordEncoder} used to validate the client secret
|
||||
*/
|
||||
public final void setPasswordEncoder(PasswordEncoder passwordEncoder) {
|
||||
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
|
||||
this.clientSecretAuthenticationProvider.setPasswordEncoder(passwordEncoder);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
protected void setProviderSettings(ProviderSettings providerSettings) {
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,34 +93,14 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
|
||||
OAuth2ClientAuthenticationToken clientAuthentication =
|
||||
(OAuth2ClientAuthenticationToken) authentication;
|
||||
|
||||
String clientId = clientAuthentication.getPrincipal().toString();
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
|
||||
if (registeredClient == null) {
|
||||
throwInvalidClient();
|
||||
if (JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
|
||||
return this.jwtClientAssertionAuthenticationProvider.authenticate(authentication);
|
||||
} else if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) ||
|
||||
ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {
|
||||
return this.clientSecretAuthenticationProvider.authenticate(authentication);
|
||||
} else {
|
||||
return this.publicClientAuthenticationProvider.authenticate(authentication);
|
||||
}
|
||||
|
||||
if (!registeredClient.getClientAuthenticationMethods().contains(
|
||||
clientAuthentication.getClientAuthenticationMethod())) {
|
||||
throwInvalidClient();
|
||||
}
|
||||
|
||||
boolean authenticatedCredentials = false;
|
||||
|
||||
if (clientAuthentication.getCredentials() != null) {
|
||||
String clientSecret = clientAuthentication.getCredentials().toString();
|
||||
if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
|
||||
throwInvalidClient();
|
||||
}
|
||||
authenticatedCredentials = true;
|
||||
}
|
||||
|
||||
authenticatedCredentials = authenticatedCredentials ||
|
||||
authenticatePkceIfAvailable(clientAuthentication, registeredClient);
|
||||
if (!authenticatedCredentials) {
|
||||
throwInvalidClient();
|
||||
}
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(registeredClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -129,67 +108,4 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
|
||||
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private boolean authenticatePkceIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
|
||||
Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
|
||||
if (CollectionUtils.isEmpty(parameters) || !authorizationCodeGrant(parameters)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
(String) parameters.get(OAuth2ParameterNames.CODE),
|
||||
AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
throwInvalidClient();
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
||||
OAuth2AuthorizationRequest.class.getName());
|
||||
|
||||
String codeChallenge = (String) authorizationRequest.getAdditionalParameters()
|
||||
.get(PkceParameterNames.CODE_CHALLENGE);
|
||||
if (!StringUtils.hasText(codeChallenge) &&
|
||||
registeredClient.getClientSettings().requireProofKey()) {
|
||||
throwInvalidClient();
|
||||
}
|
||||
|
||||
String codeChallengeMethod = (String) authorizationRequest.getAdditionalParameters()
|
||||
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
|
||||
String codeVerifier = (String) parameters.get(PkceParameterNames.CODE_VERIFIER);
|
||||
if (!codeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) {
|
||||
throwInvalidClient();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean authorizationCodeGrant(Map<String, Object> parameters) {
|
||||
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
|
||||
parameters.get(OAuth2ParameterNames.GRANT_TYPE)) &&
|
||||
parameters.get(OAuth2ParameterNames.CODE) != null;
|
||||
}
|
||||
|
||||
private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
|
||||
if (!StringUtils.hasText(codeVerifier)) {
|
||||
return false;
|
||||
} else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) {
|
||||
return codeVerifier.equals(codeChallenge);
|
||||
} else if ("S256".equals(codeChallengeMethod)) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
|
||||
String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
return encodedVerifier.equals(codeChallenge);
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
// It is unlikely that SHA-256 is not available on the server. If it is not available,
|
||||
// there will likely be bigger issues as well. We default to SERVER_ERROR.
|
||||
}
|
||||
}
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR));
|
||||
}
|
||||
|
||||
private static void throwInvalidClient() {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,17 +15,18 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.Transient;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation used for OAuth 2.0 Client Authentication.
|
||||
*
|
||||
@@ -35,98 +36,97 @@ import java.util.Map;
|
||||
* @since 0.0.1
|
||||
* @see AbstractAuthenticationToken
|
||||
* @see RegisteredClient
|
||||
* @see OAuth2ClientAuthenticationProvider
|
||||
* @see JwtClientAssertionAuthenticationProvider
|
||||
* @see ClientSecretAuthenticationProvider
|
||||
* @see PublicClientAuthenticationProvider
|
||||
*/
|
||||
@Transient
|
||||
public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private ClientAuthenticationMethod clientAuthenticationMethod;
|
||||
private Map<String, Object> additionalParameters;
|
||||
private RegisteredClient registeredClient;
|
||||
private final String clientId;
|
||||
private final RegisteredClient registeredClient;
|
||||
private final ClientAuthenticationMethod clientAuthenticationMethod;
|
||||
private final Object credentials;
|
||||
private final Map<String, Object> additionalParameters;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param clientId the client identifier
|
||||
* @param clientSecret the client secret
|
||||
* @param clientAuthenticationMethod the authentication method used by the client
|
||||
* @param credentials the client credentials
|
||||
* @param additionalParameters the additional parameters
|
||||
*/
|
||||
public OAuth2ClientAuthenticationToken(String clientId, String clientSecret,
|
||||
ClientAuthenticationMethod clientAuthenticationMethod,
|
||||
@Nullable Map<String, Object> additionalParameters) {
|
||||
this(clientId, additionalParameters);
|
||||
Assert.hasText(clientSecret, "clientSecret cannot be empty");
|
||||
Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
|
||||
this.clientSecret = clientSecret;
|
||||
this.clientAuthenticationMethod = clientAuthenticationMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param clientId the client identifier
|
||||
* @param additionalParameters the additional parameters
|
||||
*/
|
||||
public OAuth2ClientAuthenticationToken(String clientId,
|
||||
@Nullable Map<String, Object> additionalParameters) {
|
||||
public OAuth2ClientAuthenticationToken(String clientId, ClientAuthenticationMethod clientAuthenticationMethod,
|
||||
@Nullable Object credentials, @Nullable Map<String, Object> additionalParameters) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
|
||||
this.clientId = clientId;
|
||||
this.additionalParameters = additionalParameters != null ?
|
||||
Collections.unmodifiableMap(additionalParameters) : null;
|
||||
this.clientAuthenticationMethod = ClientAuthenticationMethod.NONE;
|
||||
this.registeredClient = null;
|
||||
this.clientAuthenticationMethod = clientAuthenticationMethod;
|
||||
this.credentials = credentials;
|
||||
this.additionalParameters = Collections.unmodifiableMap(
|
||||
additionalParameters != null ? additionalParameters : Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param registeredClient the registered client
|
||||
* @param registeredClient the authenticated registered client
|
||||
* @param clientAuthenticationMethod the authentication method used by the client
|
||||
* @param credentials the client credentials
|
||||
*/
|
||||
public OAuth2ClientAuthenticationToken(RegisteredClient registeredClient) {
|
||||
public OAuth2ClientAuthenticationToken(RegisteredClient registeredClient, ClientAuthenticationMethod clientAuthenticationMethod,
|
||||
@Nullable Object credentials) {
|
||||
super(Collections.emptyList());
|
||||
Assert.notNull(registeredClient, "registeredClient cannot be null");
|
||||
Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
|
||||
this.clientId = registeredClient.getClientId();
|
||||
this.registeredClient = registeredClient;
|
||||
this.clientAuthenticationMethod = clientAuthenticationMethod;
|
||||
this.credentials = credentials;
|
||||
this.additionalParameters = Collections.unmodifiableMap(Collections.emptyMap());
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.registeredClient != null ?
|
||||
this.registeredClient.getClientId() :
|
||||
this.clientId;
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.clientSecret;
|
||||
return this.credentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional parameters
|
||||
* Returns the authenticated {@link RegisteredClient registered client}, or {@code null} if not authenticated.
|
||||
*
|
||||
* @return the additional parameters
|
||||
* @return the authenticated {@link RegisteredClient}, or {@code null} if not authenticated
|
||||
*/
|
||||
public @Nullable Map<String, Object> getAdditionalParameters() {
|
||||
return this.additionalParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link RegisteredClient registered client}.
|
||||
*
|
||||
* @return the {@link RegisteredClient}
|
||||
*/
|
||||
public @Nullable RegisteredClient getRegisteredClient() {
|
||||
@Nullable
|
||||
public RegisteredClient getRegisteredClient() {
|
||||
return this.registeredClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ClientAuthenticationMethod client authentication method}.
|
||||
* Returns the {@link ClientAuthenticationMethod authentication method} used by the client.
|
||||
*
|
||||
* @return the {@link ClientAuthenticationMethod}
|
||||
* @return the {@link ClientAuthenticationMethod} used by the client
|
||||
*/
|
||||
public @Nullable ClientAuthenticationMethod getClientAuthenticationMethod() {
|
||||
public ClientAuthenticationMethod getClientAuthenticationMethod() {
|
||||
return this.clientAuthenticationMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional parameters.
|
||||
*
|
||||
* @return the additional parameters
|
||||
*/
|
||||
public Map<String, Object> getAdditionalParameters() {
|
||||
return this.additionalParameters;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,28 +17,34 @@ package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@@ -53,40 +59,69 @@ import static org.springframework.security.oauth2.server.authorization.authentic
|
||||
* @see OAuth2ClientCredentialsAuthenticationToken
|
||||
* @see OAuth2AccessTokenAuthenticationToken
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see JwtEncoder
|
||||
* @see OAuth2TokenCustomizer
|
||||
* @see JwtEncodingContext
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.4">Section 4.4 Client Credentials Grant</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.4.2">Section 4.4.2 Access Token Request</a>
|
||||
* @see OAuth2TokenGenerator
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.4">Section 4.4 Client Credentials Grant</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.2">Section 4.4.2 Access Token Request</a>
|
||||
*/
|
||||
public class OAuth2ClientCredentialsAuthenticationProvider implements AuthenticationProvider {
|
||||
public final class OAuth2ClientCredentialsAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final JwtEncoder jwtEncoder;
|
||||
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
|
||||
private ProviderSettings providerSettings;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
|
||||
// TODO Remove after removing @Deprecated OAuth2ClientCredentialsAuthenticationProvider(OAuth2AuthorizationService, JwtEncoder)
|
||||
private JwtGenerator jwtGenerator;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @deprecated Use {@link #OAuth2ClientCredentialsAuthenticationProvider(OAuth2AuthorizationService, OAuth2TokenGenerator)} instead
|
||||
* @param authorizationService the authorization service
|
||||
* @param jwtEncoder the jwt encoder
|
||||
*/
|
||||
@Deprecated
|
||||
public OAuth2ClientCredentialsAuthenticationProvider(OAuth2AuthorizationService authorizationService,
|
||||
JwtEncoder jwtEncoder) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.jwtEncoder = jwtEncoder;
|
||||
this.jwtGenerator = new JwtGenerator(jwtEncoder);
|
||||
this.tokenGenerator = new DelegatingOAuth2TokenGenerator(
|
||||
this.jwtGenerator, new OAuth2AccessTokenGenerator());
|
||||
}
|
||||
|
||||
public final void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param authorizationService the authorization service
|
||||
* @param tokenGenerator the token generator
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public OAuth2ClientCredentialsAuthenticationProvider(OAuth2AuthorizationService authorizationService,
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenCustomizer} that customizes the
|
||||
* {@link JwtEncodingContext.Builder#headers(Consumer) headers} and/or
|
||||
* {@link JwtEncodingContext.Builder#claims(Consumer) claims} for the generated {@link Jwt}.
|
||||
*
|
||||
* @deprecated Use {@link JwtGenerator#setJwtCustomizer(OAuth2TokenCustomizer)} instead
|
||||
* @param jwtCustomizer the {@link OAuth2TokenCustomizer} that customizes the headers and/or claims for the generated {@code Jwt}
|
||||
*/
|
||||
@Deprecated
|
||||
public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
|
||||
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
|
||||
this.jwtCustomizer = jwtCustomizer;
|
||||
if (this.jwtGenerator != null) {
|
||||
this.jwtGenerator.setJwtCustomizer(jwtCustomizer);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
@Deprecated
|
||||
protected void setProviderSettings(ProviderSettings providerSettings) {
|
||||
this.providerSettings = providerSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,30 +134,24 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.CLIENT_CREDENTIALS)) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT));
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
|
||||
}
|
||||
|
||||
Set<String> authorizedScopes = registeredClient.getScopes(); // Default to configured scopes
|
||||
if (!CollectionUtils.isEmpty(clientCredentialsAuthentication.getScopes())) {
|
||||
Set<String> unauthorizedScopes = clientCredentialsAuthentication.getScopes().stream()
|
||||
.filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope))
|
||||
.collect(Collectors.toSet());
|
||||
if (!CollectionUtils.isEmpty(unauthorizedScopes)) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE));
|
||||
for (String requestedScope : clientCredentialsAuthentication.getScopes()) {
|
||||
if (!registeredClient.getScopes().contains(requestedScope)) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
|
||||
}
|
||||
}
|
||||
authorizedScopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
|
||||
}
|
||||
|
||||
String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null;
|
||||
|
||||
JoseHeader.Builder headersBuilder = JwtUtils.headers();
|
||||
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(
|
||||
registeredClient, issuer, clientPrincipal.getName(), authorizedScopes);
|
||||
|
||||
// @formatter:off
|
||||
JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
|
||||
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(clientPrincipal)
|
||||
.providerContext(ProviderContextHolder.getProviderContext())
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
@@ -130,26 +159,30 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
this.jwtCustomizer.customize(context);
|
||||
|
||||
JoseHeader headers = context.getHeaders().build();
|
||||
JwtClaimsSet claims = context.getClaims().build();
|
||||
Jwt jwtAccessToken = this.jwtEncoder.encode(headers, claims);
|
||||
|
||||
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (generatedAccessToken == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the access token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
|
||||
jwtAccessToken.getExpiresAt(), authorizedScopes);
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
|
||||
// @formatter:off
|
||||
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.principalName(clientPrincipal.getName())
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.token(accessToken,
|
||||
(metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, jwtAccessToken.getClaims()))
|
||||
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes)
|
||||
.build();
|
||||
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
|
||||
// @formatter:on
|
||||
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||
authorizationBuilder.token(accessToken, (metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
|
||||
} else {
|
||||
authorizationBuilder.accessToken(accessToken);
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
@@ -160,4 +193,5 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2ClientCredentialsAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,36 +16,45 @@
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeader;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
|
||||
@@ -55,45 +64,90 @@ import static org.springframework.security.oauth2.server.authorization.authentic
|
||||
*
|
||||
* @author Alexey Nesterov
|
||||
* @author Joe Grandja
|
||||
* @author Anoop Garlapati
|
||||
* @since 0.0.3
|
||||
* @see OAuth2RefreshTokenAuthenticationToken
|
||||
* @see OAuth2AccessTokenAuthenticationToken
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see JwtEncoder
|
||||
* @see OAuth2TokenCustomizer
|
||||
* @see JwtEncodingContext
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.5">Section 1.5 Refresh Token Grant</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
|
||||
* @see OAuth2TokenGenerator
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-1.5">Section 1.5 Refresh Token Grant</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
|
||||
*/
|
||||
public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final StringKeyGenerator TOKEN_GENERATOR = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
||||
public final class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final JwtEncoder jwtEncoder;
|
||||
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
|
||||
private ProviderSettings providerSettings;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
|
||||
// TODO Remove after removing @Deprecated OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService, JwtEncoder)
|
||||
private JwtGenerator jwtGenerator;
|
||||
|
||||
@Deprecated
|
||||
private Supplier<String> refreshTokenGenerator;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @deprecated Use {@link #OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService, OAuth2TokenGenerator)} instead
|
||||
* @param authorizationService the authorization service
|
||||
* @param jwtEncoder the jwt encoder
|
||||
*/
|
||||
@Deprecated
|
||||
public OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService authorizationService,
|
||||
JwtEncoder jwtEncoder) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.jwtEncoder = jwtEncoder;
|
||||
this.jwtGenerator = new JwtGenerator(jwtEncoder);
|
||||
this.tokenGenerator = new DelegatingOAuth2TokenGenerator(this.jwtGenerator,
|
||||
new OAuth2AccessTokenGenerator(), new OAuth2RefreshTokenGenerator());
|
||||
}
|
||||
|
||||
public final void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
|
||||
/**
|
||||
* Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param authorizationService the authorization service
|
||||
* @param tokenGenerator the token generator
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService authorizationService,
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenCustomizer} that customizes the
|
||||
* {@link JwtEncodingContext.Builder#headers(Consumer) headers} and/or
|
||||
* {@link JwtEncodingContext.Builder#claims(Consumer) claims} for the generated {@link Jwt}.
|
||||
*
|
||||
* @deprecated Use {@link JwtGenerator#setJwtCustomizer(OAuth2TokenCustomizer)} instead
|
||||
* @param jwtCustomizer the {@link OAuth2TokenCustomizer} that customizes the headers and/or claims for the generated {@code Jwt}
|
||||
*/
|
||||
@Deprecated
|
||||
public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
|
||||
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
|
||||
this.jwtCustomizer = jwtCustomizer;
|
||||
if (this.jwtGenerator != null) {
|
||||
this.jwtGenerator.setJwtCustomizer(jwtCustomizer);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
/**
|
||||
* Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}.
|
||||
*
|
||||
* @deprecated Use {@link OAuth2RefreshTokenGenerator} instead
|
||||
* @param refreshTokenGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}
|
||||
*/
|
||||
@Deprecated
|
||||
public void setRefreshTokenGenerator(Supplier<String> refreshTokenGenerator) {
|
||||
Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null");
|
||||
this.refreshTokenGenerator = refreshTokenGenerator;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
protected void setProviderSettings(ProviderSettings providerSettings) {
|
||||
this.providerSettings = providerSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -108,24 +162,23 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
refreshTokenAuthentication.getRefreshToken(), OAuth2TokenType.REFRESH_TOKEN);
|
||||
if (authorization == null) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT));
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = authorization.getRefreshToken();
|
||||
Instant refreshTokenExpiresAt = refreshToken.getToken().getExpiresAt();
|
||||
if (refreshTokenExpiresAt.isBefore(Instant.now())) {
|
||||
if (!refreshToken.isActive()) {
|
||||
// As per https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
// invalid_grant: The provided authorization grant (e.g., authorization code,
|
||||
// resource owner credentials) or refresh token is invalid, expired, revoked [...].
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
// As per https://tools.ietf.org/html/rfc6749#section-6
|
||||
@@ -134,64 +187,95 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
|
||||
Set<String> scopes = refreshTokenAuthentication.getScopes();
|
||||
Set<String> authorizedScopes = authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
|
||||
if (!authorizedScopes.containsAll(scopes)) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE));
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
|
||||
}
|
||||
if (scopes.isEmpty()) {
|
||||
scopes = authorizedScopes;
|
||||
}
|
||||
|
||||
if (refreshToken.isInvalidated()) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
}
|
||||
|
||||
String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null;
|
||||
|
||||
JoseHeader.Builder headersBuilder = JwtUtils.headers();
|
||||
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(
|
||||
registeredClient, issuer, authorization.getPrincipalName(), scopes);
|
||||
|
||||
// @formatter:off
|
||||
JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(authorization.getAttribute(Principal.class.getName()))
|
||||
.providerContext(ProviderContextHolder.getProviderContext())
|
||||
.authorization(authorization)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
|
||||
.authorizedScopes(scopes)
|
||||
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||
.authorizationGrant(refreshTokenAuthentication)
|
||||
.build();
|
||||
.authorizationGrant(refreshTokenAuthentication);
|
||||
// @formatter:on
|
||||
|
||||
this.jwtCustomizer.customize(context);
|
||||
|
||||
JoseHeader headers = context.getHeaders().build();
|
||||
JwtClaimsSet claims = context.getClaims().build();
|
||||
Jwt jwtAccessToken = this.jwtEncoder.encode(headers, claims);
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
|
||||
|
||||
// ----- Access token -----
|
||||
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (generatedAccessToken == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the access token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
|
||||
jwtAccessToken.getExpiresAt(), scopes);
|
||||
|
||||
TokenSettings tokenSettings = registeredClient.getTokenSettings();
|
||||
|
||||
OAuth2RefreshToken currentRefreshToken = refreshToken.getToken();
|
||||
if (!tokenSettings.reuseRefreshTokens()) {
|
||||
currentRefreshToken = generateRefreshToken(tokenSettings.refreshTokenTimeToLive());
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false);
|
||||
});
|
||||
} else {
|
||||
authorizationBuilder.accessToken(accessToken);
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
authorization = OAuth2Authorization.from(authorization)
|
||||
.token(accessToken,
|
||||
(metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, jwtAccessToken.getClaims()))
|
||||
.refreshToken(currentRefreshToken)
|
||||
.build();
|
||||
// @formatter:on
|
||||
// ----- Refresh token -----
|
||||
OAuth2RefreshToken currentRefreshToken = refreshToken.getToken();
|
||||
if (!registeredClient.getTokenSettings().isReuseRefreshTokens()) {
|
||||
if (this.refreshTokenGenerator != null) {
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive());
|
||||
currentRefreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt);
|
||||
} else {
|
||||
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the refresh token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
currentRefreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||
}
|
||||
authorizationBuilder.refreshToken(currentRefreshToken);
|
||||
}
|
||||
|
||||
// ----- ID token -----
|
||||
OidcIdToken idToken;
|
||||
if (authorizedScopes.contains(OidcScopes.OPENID)) {
|
||||
tokenContext = tokenContextBuilder.tokenType(ID_TOKEN_TOKEN_TYPE).build();
|
||||
OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (!(generatedIdToken instanceof Jwt)) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the ID token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
|
||||
generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
|
||||
authorizationBuilder.token(idToken, (metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
|
||||
} else {
|
||||
idToken = null;
|
||||
}
|
||||
|
||||
authorization = authorizationBuilder.build();
|
||||
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
Map<String, Object> additionalParameters = Collections.emptyMap();
|
||||
if (idToken != null) {
|
||||
additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
|
||||
}
|
||||
|
||||
return new OAuth2AccessTokenAuthenticationToken(
|
||||
registeredClient, clientPrincipal, accessToken, currentRefreshToken);
|
||||
registeredClient, clientPrincipal, accessToken, currentRefreshToken, additionalParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -199,9 +283,4 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
|
||||
return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
static OAuth2RefreshToken generateRefreshToken(Duration tokenTimeToLive) {
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(tokenTimeToLive);
|
||||
return new OAuth2RefreshToken2(TOKEN_GENERATOR.generateKey(), issuedAt, expiresAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,24 +15,26 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
|
||||
|
||||
@@ -47,7 +49,10 @@ import static org.springframework.security.oauth2.server.authorization.authentic
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.1">Section 2.1 Introspection Request</a>
|
||||
*/
|
||||
public class OAuth2TokenIntrospectionAuthenticationProvider implements AuthenticationProvider {
|
||||
public final class OAuth2TokenIntrospectionAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
|
||||
private static final TypeDescriptor LIST_STRING_TYPE_DESCRIPTOR =
|
||||
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class));
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
@@ -102,8 +107,15 @@ public class OAuth2TokenIntrospectionAuthenticationProvider implements Authentic
|
||||
private static OAuth2TokenIntrospection withActiveTokenClaims(
|
||||
OAuth2Authorization.Token<AbstractOAuth2Token> authorizedToken, RegisteredClient authorizedClient) {
|
||||
|
||||
OAuth2TokenIntrospection.Builder tokenClaims = OAuth2TokenIntrospection.builder(true)
|
||||
.clientId(authorizedClient.getClientId());
|
||||
OAuth2TokenIntrospection.Builder tokenClaims;
|
||||
if (!CollectionUtils.isEmpty(authorizedToken.getClaims())) {
|
||||
Map<String, Object> claims = convertClaimsIfNecessary(authorizedToken.getClaims());
|
||||
tokenClaims = OAuth2TokenIntrospection.withClaims(claims).active(true);
|
||||
} else {
|
||||
tokenClaims = OAuth2TokenIntrospection.builder(true);
|
||||
}
|
||||
|
||||
tokenClaims.clientId(authorizedClient.getClientId());
|
||||
|
||||
// TODO Set "username"
|
||||
|
||||
@@ -117,31 +129,43 @@ public class OAuth2TokenIntrospectionAuthenticationProvider implements Authentic
|
||||
|
||||
if (OAuth2AccessToken.class.isAssignableFrom(token.getClass())) {
|
||||
OAuth2AccessToken accessToken = (OAuth2AccessToken) token;
|
||||
tokenClaims.scopes(scopes -> scopes.addAll(accessToken.getScopes()));
|
||||
tokenClaims.tokenType(accessToken.getTokenType().getValue());
|
||||
|
||||
Map<String, Object> claims = authorizedToken.getClaims();
|
||||
if (!CollectionUtils.isEmpty(claims)) {
|
||||
// Assuming JWT as it's the only (currently) supported access token format
|
||||
JwtClaimAccessor jwtClaims = () -> claims;
|
||||
|
||||
Instant notBefore = jwtClaims.getNotBefore();
|
||||
if (notBefore != null) {
|
||||
tokenClaims.notBefore(notBefore);
|
||||
}
|
||||
tokenClaims.subject(jwtClaims.getSubject());
|
||||
List<String> audience = jwtClaims.getAudience();
|
||||
if (!CollectionUtils.isEmpty(audience)) {
|
||||
tokenClaims.audiences(audiences -> audiences.addAll(audience));
|
||||
}
|
||||
tokenClaims.issuer(jwtClaims.getIssuer().toExternalForm());
|
||||
String jti = jwtClaims.getId();
|
||||
if (StringUtils.hasText(jti)) {
|
||||
tokenClaims.id(jti);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tokenClaims.build();
|
||||
}
|
||||
|
||||
private static Map<String, Object> convertClaimsIfNecessary(Map<String, Object> claims) {
|
||||
Map<String, Object> convertedClaims = new HashMap<>(claims);
|
||||
|
||||
Object value = claims.get(OAuth2TokenIntrospectionClaimNames.ISS);
|
||||
if (value != null && !(value instanceof URL)) {
|
||||
URL convertedValue = ClaimConversionService.getSharedInstance()
|
||||
.convert(value, URL.class);
|
||||
if (convertedValue != null) {
|
||||
convertedClaims.put(OAuth2TokenIntrospectionClaimNames.ISS, convertedValue);
|
||||
}
|
||||
}
|
||||
|
||||
value = claims.get(OAuth2TokenIntrospectionClaimNames.SCOPE);
|
||||
if (value != null && !(value instanceof List)) {
|
||||
Object convertedValue = ClaimConversionService.getSharedInstance()
|
||||
.convert(value, OBJECT_TYPE_DESCRIPTOR, LIST_STRING_TYPE_DESCRIPTOR);
|
||||
if (convertedValue != null) {
|
||||
convertedClaims.put(OAuth2TokenIntrospectionClaimNames.SCOPE, convertedValue);
|
||||
}
|
||||
}
|
||||
|
||||
value = claims.get(OAuth2TokenIntrospectionClaimNames.AUD);
|
||||
if (value != null && !(value instanceof List)) {
|
||||
Object convertedValue = ClaimConversionService.getSharedInstance()
|
||||
.convert(value, OBJECT_TYPE_DESCRIPTOR, LIST_STRING_TYPE_DESCRIPTOR);
|
||||
if (convertedValue != null) {
|
||||
convertedClaims.put(OAuth2TokenIntrospectionClaimNames.AUD, convertedValue);
|
||||
}
|
||||
}
|
||||
|
||||
return convertedClaims;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
@@ -39,7 +38,7 @@ import static org.springframework.security.oauth2.server.authorization.authentic
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-2.1">Section 2.1 Revocation Request</a>
|
||||
*/
|
||||
public class OAuth2TokenRevocationAuthenticationProvider implements AuthenticationProvider {
|
||||
public final class OAuth2TokenRevocationAuthenticationProvider implements AuthenticationProvider {
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
/**
|
||||
@@ -69,7 +68,7 @@ public class OAuth2TokenRevocationAuthenticationProvider implements Authenticati
|
||||
}
|
||||
|
||||
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<AbstractOAuth2Token> token = authorization.getToken(tokenRevocationAuthentication.getToken());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,15 +15,15 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation used for OAuth 2.0 Token Revocation.
|
||||
*
|
||||
@@ -62,7 +62,7 @@ public class OAuth2TokenRevocationAuthenticationToken extends AbstractAuthentica
|
||||
* @param revokedToken the revoked token
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
*/
|
||||
public OAuth2TokenRevocationAuthenticationToken(AbstractOAuth2Token revokedToken,
|
||||
public OAuth2TokenRevocationAuthenticationToken(OAuth2Token revokedToken,
|
||||
Authentication clientPrincipal) {
|
||||
super(Collections.emptyList());
|
||||
Assert.notNull(revokedToken, "revokedToken cannot be null");
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation used for OAuth 2.0 Public Client Authentication,
|
||||
* which authenticates the {@link PkceParameterNames#CODE_VERIFIER code_verifier} parameter.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.3
|
||||
* @see AuthenticationProvider
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
*/
|
||||
public final class PublicClientAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final CodeVerifierAuthenticator codeVerifierAuthenticator;
|
||||
|
||||
/**
|
||||
* Constructs a {@code PublicClientAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param registeredClientRepository the repository of registered clients
|
||||
* @param authorizationService the authorization service
|
||||
*/
|
||||
public PublicClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
|
||||
OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2ClientAuthenticationToken clientAuthentication =
|
||||
(OAuth2ClientAuthenticationToken) authentication;
|
||||
|
||||
if (!ClientAuthenticationMethod.NONE.equals(clientAuthentication.getClientAuthenticationMethod())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String clientId = clientAuthentication.getPrincipal().toString();
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
|
||||
if (registeredClient == null) {
|
||||
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
|
||||
}
|
||||
|
||||
if (!registeredClient.getClientAuthenticationMethods().contains(
|
||||
clientAuthentication.getClientAuthenticationMethod())) {
|
||||
throwInvalidClient("authentication_method");
|
||||
}
|
||||
|
||||
// Validate the "code_verifier" parameter for the public client
|
||||
this.codeVerifierAuthenticator.authenticateRequired(clientAuthentication, registeredClient);
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(registeredClient,
|
||||
clientAuthentication.getClientAuthenticationMethod(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private static void throwInvalidClient(String parameterName) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Client authentication failed: " + parameterName,
|
||||
ERROR_URI
|
||||
);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.client;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.sql.Types;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.JdbcOperations;
|
||||
import org.springframework.jdbc.core.PreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.core.SqlParameterValue;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ConfigurationSettingNames;
|
||||
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A JDBC implementation of a {@link RegisteredClientRepository} that uses a
|
||||
* {@link JdbcOperations} for {@link RegisteredClient} persistence.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This {@code RegisteredClientRepository} depends on the table definition described in
|
||||
* "classpath:org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql" and
|
||||
* therefore MUST be defined in the database schema.
|
||||
*
|
||||
* @author Rafal Lewczuk
|
||||
* @author Joe Grandja
|
||||
* @author Ovidiu Popa
|
||||
* @since 0.1.2
|
||||
* @see RegisteredClientRepository
|
||||
* @see RegisteredClient
|
||||
* @see JdbcOperations
|
||||
* @see RowMapper
|
||||
*/
|
||||
public class JdbcRegisteredClientRepository implements RegisteredClientRepository {
|
||||
|
||||
// @formatter:off
|
||||
private static final String COLUMN_NAMES = "id, "
|
||||
+ "client_id, "
|
||||
+ "client_id_issued_at, "
|
||||
+ "client_secret, "
|
||||
+ "client_secret_expires_at, "
|
||||
+ "client_name, "
|
||||
+ "client_authentication_methods, "
|
||||
+ "authorization_grant_types, "
|
||||
+ "redirect_uris, "
|
||||
+ "scopes, "
|
||||
+ "client_settings,"
|
||||
+ "token_settings";
|
||||
// @formatter:on
|
||||
|
||||
private static final String TABLE_NAME = "oauth2_registered_client";
|
||||
|
||||
private static final String PK_FILTER = "id = ?";
|
||||
|
||||
private static final String LOAD_REGISTERED_CLIENT_SQL = "SELECT " + COLUMN_NAMES + " FROM " + TABLE_NAME + " WHERE ";
|
||||
|
||||
// @formatter:off
|
||||
private static final String INSERT_REGISTERED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME
|
||||
+ "(" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
private static final String UPDATE_REGISTERED_CLIENT_SQL = "UPDATE " + TABLE_NAME
|
||||
+ " SET client_name = ?, client_authentication_methods = ?, authorization_grant_types = ?,"
|
||||
+ " redirect_uris = ?, scopes = ?, client_settings = ?, token_settings = ?"
|
||||
+ " WHERE " + PK_FILTER;
|
||||
// @formatter:on
|
||||
|
||||
private final JdbcOperations jdbcOperations;
|
||||
private RowMapper<RegisteredClient> registeredClientRowMapper;
|
||||
private Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper;
|
||||
|
||||
/**
|
||||
* Constructs a {@code JdbcRegisteredClientRepository} using the provided parameters.
|
||||
*
|
||||
* @param jdbcOperations the JDBC operations
|
||||
*/
|
||||
public JdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
|
||||
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
|
||||
this.jdbcOperations = jdbcOperations;
|
||||
this.registeredClientRowMapper = new RegisteredClientRowMapper();
|
||||
this.registeredClientParametersMapper = new RegisteredClientParametersMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(RegisteredClient registeredClient) {
|
||||
Assert.notNull(registeredClient, "registeredClient cannot be null");
|
||||
RegisteredClient existingRegisteredClient = findBy(PK_FILTER,
|
||||
registeredClient.getId());
|
||||
if (existingRegisteredClient != null) {
|
||||
updateRegisteredClient(registeredClient);
|
||||
} else {
|
||||
insertRegisteredClient(registeredClient);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRegisteredClient(RegisteredClient registeredClient) {
|
||||
List<SqlParameterValue> parameters = new ArrayList<>(this.registeredClientParametersMapper.apply(registeredClient));
|
||||
SqlParameterValue id = parameters.remove(0);
|
||||
parameters.remove(0); // remove client_id
|
||||
parameters.remove(0); // remove client_id_issued_at
|
||||
parameters.remove(0); // remove client_secret
|
||||
parameters.remove(0); // remove client_secret_expires_at
|
||||
parameters.add(id);
|
||||
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
|
||||
this.jdbcOperations.update(UPDATE_REGISTERED_CLIENT_SQL, pss);
|
||||
}
|
||||
|
||||
private void insertRegisteredClient(RegisteredClient registeredClient) {
|
||||
List<SqlParameterValue> parameters = this.registeredClientParametersMapper.apply(registeredClient);
|
||||
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
|
||||
this.jdbcOperations.update(INSERT_REGISTERED_CLIENT_SQL, pss);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegisteredClient findById(String id) {
|
||||
Assert.hasText(id, "id cannot be empty");
|
||||
return findBy("id = ?", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegisteredClient findByClientId(String clientId) {
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
return findBy("client_id = ?", clientId);
|
||||
}
|
||||
|
||||
private RegisteredClient findBy(String filter, Object... args) {
|
||||
List<RegisteredClient> result = this.jdbcOperations.query(
|
||||
LOAD_REGISTERED_CLIENT_SQL + filter, this.registeredClientRowMapper, args);
|
||||
return !result.isEmpty() ? result.get(0) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RowMapper} used for mapping the current row in {@code java.sql.ResultSet} to {@link RegisteredClient}.
|
||||
* The default is {@link RegisteredClientRowMapper}.
|
||||
*
|
||||
* @param registeredClientRowMapper the {@link RowMapper} used for mapping the current row in {@code ResultSet} to {@link RegisteredClient}
|
||||
*/
|
||||
public final void setRegisteredClientRowMapper(RowMapper<RegisteredClient> registeredClientRowMapper) {
|
||||
Assert.notNull(registeredClientRowMapper, "registeredClientRowMapper cannot be null");
|
||||
this.registeredClientRowMapper = registeredClientRowMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Function} used for mapping {@link RegisteredClient} to a {@code List} of {@link SqlParameterValue}.
|
||||
* The default is {@link RegisteredClientParametersMapper}.
|
||||
*
|
||||
* @param registeredClientParametersMapper the {@code Function} used for mapping {@link RegisteredClient} to a {@code List} of {@link SqlParameterValue}
|
||||
*/
|
||||
public final void setRegisteredClientParametersMapper(Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper) {
|
||||
Assert.notNull(registeredClientParametersMapper, "registeredClientParametersMapper cannot be null");
|
||||
this.registeredClientParametersMapper = registeredClientParametersMapper;
|
||||
}
|
||||
|
||||
protected final JdbcOperations getJdbcOperations() {
|
||||
return this.jdbcOperations;
|
||||
}
|
||||
|
||||
protected final RowMapper<RegisteredClient> getRegisteredClientRowMapper() {
|
||||
return this.registeredClientRowMapper;
|
||||
}
|
||||
|
||||
protected final Function<RegisteredClient, List<SqlParameterValue>> getRegisteredClientParametersMapper() {
|
||||
return this.registeredClientParametersMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default {@link RowMapper} that maps the current row in
|
||||
* {@code java.sql.ResultSet} to {@link RegisteredClient}.
|
||||
*/
|
||||
public static class RegisteredClientRowMapper implements RowMapper<RegisteredClient> {
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public RegisteredClientRowMapper() {
|
||||
ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
|
||||
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
|
||||
this.objectMapper.registerModules(securityModules);
|
||||
this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegisteredClient mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
Timestamp clientIdIssuedAt = rs.getTimestamp("client_id_issued_at");
|
||||
Timestamp clientSecretExpiresAt = rs.getTimestamp("client_secret_expires_at");
|
||||
Set<String> clientAuthenticationMethods = StringUtils.commaDelimitedListToSet(rs.getString("client_authentication_methods"));
|
||||
Set<String> authorizationGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorization_grant_types"));
|
||||
Set<String> redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirect_uris"));
|
||||
Set<String> clientScopes = StringUtils.commaDelimitedListToSet(rs.getString("scopes"));
|
||||
|
||||
// @formatter:off
|
||||
RegisteredClient.Builder builder = RegisteredClient.withId(rs.getString("id"))
|
||||
.clientId(rs.getString("client_id"))
|
||||
.clientIdIssuedAt(clientIdIssuedAt != null ? clientIdIssuedAt.toInstant() : null)
|
||||
.clientSecret(rs.getString("client_secret"))
|
||||
.clientSecretExpiresAt(clientSecretExpiresAt != null ? clientSecretExpiresAt.toInstant() : null)
|
||||
.clientName(rs.getString("client_name"))
|
||||
.clientAuthenticationMethods((authenticationMethods) ->
|
||||
clientAuthenticationMethods.forEach(authenticationMethod ->
|
||||
authenticationMethods.add(resolveClientAuthenticationMethod(authenticationMethod))))
|
||||
.authorizationGrantTypes((grantTypes) ->
|
||||
authorizationGrantTypes.forEach(grantType ->
|
||||
grantTypes.add(resolveAuthorizationGrantType(grantType))))
|
||||
.redirectUris((uris) -> uris.addAll(redirectUris))
|
||||
.scopes((scopes) -> scopes.addAll(clientScopes));
|
||||
// @formatter:on
|
||||
|
||||
Map<String, Object> clientSettingsMap = parseMap(rs.getString("client_settings"));
|
||||
builder.clientSettings(ClientSettings.withSettings(clientSettingsMap).build());
|
||||
|
||||
Map<String, Object> tokenSettingsMap = parseMap(rs.getString("token_settings"));
|
||||
TokenSettings.Builder tokenSettingsBuilder = TokenSettings.withSettings(tokenSettingsMap);
|
||||
if (!tokenSettingsMap.containsKey(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT)) {
|
||||
tokenSettingsBuilder.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED);
|
||||
}
|
||||
builder.tokenSettings(tokenSettingsBuilder.build());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public final void setObjectMapper(ObjectMapper objectMapper) {
|
||||
Assert.notNull(objectMapper, "objectMapper cannot be null");
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
protected final ObjectMapper getObjectMapper() {
|
||||
return this.objectMapper;
|
||||
}
|
||||
|
||||
private Map<String, Object> parseMap(String data) {
|
||||
try {
|
||||
return this.objectMapper.readValue(data, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
|
||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
|
||||
return AuthorizationGrantType.AUTHORIZATION_CODE;
|
||||
} else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) {
|
||||
return AuthorizationGrantType.CLIENT_CREDENTIALS;
|
||||
} else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(authorizationGrantType)) {
|
||||
return AuthorizationGrantType.REFRESH_TOKEN;
|
||||
}
|
||||
return new AuthorizationGrantType(authorizationGrantType); // Custom authorization grant type
|
||||
}
|
||||
|
||||
private static ClientAuthenticationMethod resolveClientAuthenticationMethod(String clientAuthenticationMethod) {
|
||||
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equals(clientAuthenticationMethod)) {
|
||||
return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
|
||||
} else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientAuthenticationMethod)) {
|
||||
return ClientAuthenticationMethod.CLIENT_SECRET_POST;
|
||||
} else if (ClientAuthenticationMethod.NONE.getValue().equals(clientAuthenticationMethod)) {
|
||||
return ClientAuthenticationMethod.NONE;
|
||||
}
|
||||
return new ClientAuthenticationMethod(clientAuthenticationMethod); // Custom client authentication method
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The default {@code Function} that maps {@link RegisteredClient} to a
|
||||
* {@code List} of {@link SqlParameterValue}.
|
||||
*/
|
||||
public static class RegisteredClientParametersMapper implements Function<RegisteredClient, List<SqlParameterValue>> {
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public RegisteredClientParametersMapper() {
|
||||
ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
|
||||
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
|
||||
this.objectMapper.registerModules(securityModules);
|
||||
this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SqlParameterValue> apply(RegisteredClient registeredClient) {
|
||||
Timestamp clientIdIssuedAt = registeredClient.getClientIdIssuedAt() != null ?
|
||||
Timestamp.from(registeredClient.getClientIdIssuedAt()) : Timestamp.from(Instant.now());
|
||||
|
||||
Timestamp clientSecretExpiresAt = registeredClient.getClientSecretExpiresAt() != null ?
|
||||
Timestamp.from(registeredClient.getClientSecretExpiresAt()) : null;
|
||||
|
||||
List<String> clientAuthenticationMethods = new ArrayList<>(registeredClient.getClientAuthenticationMethods().size());
|
||||
registeredClient.getClientAuthenticationMethods().forEach(clientAuthenticationMethod ->
|
||||
clientAuthenticationMethods.add(clientAuthenticationMethod.getValue()));
|
||||
|
||||
List<String> authorizationGrantTypes = new ArrayList<>(registeredClient.getAuthorizationGrantTypes().size());
|
||||
registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
|
||||
authorizationGrantTypes.add(authorizationGrantType.getValue()));
|
||||
|
||||
return Arrays.asList(
|
||||
new SqlParameterValue(Types.VARCHAR, registeredClient.getId()),
|
||||
new SqlParameterValue(Types.VARCHAR, registeredClient.getClientId()),
|
||||
new SqlParameterValue(Types.TIMESTAMP, clientIdIssuedAt),
|
||||
new SqlParameterValue(Types.VARCHAR, registeredClient.getClientSecret()),
|
||||
new SqlParameterValue(Types.TIMESTAMP, clientSecretExpiresAt),
|
||||
new SqlParameterValue(Types.VARCHAR, registeredClient.getClientName()),
|
||||
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(clientAuthenticationMethods)),
|
||||
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(authorizationGrantTypes)),
|
||||
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(registeredClient.getRedirectUris())),
|
||||
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(registeredClient.getScopes())),
|
||||
new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getClientSettings().getSettings())),
|
||||
new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getTokenSettings().getSettings())));
|
||||
}
|
||||
|
||||
public final void setObjectMapper(ObjectMapper objectMapper) {
|
||||
Assert.notNull(objectMapper, "objectMapper cannot be null");
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated See javadoc {@link RegisteredClientRepository#save(RegisteredClient)}
|
||||
*/
|
||||
@Deprecated
|
||||
public final void setPasswordEncoder(PasswordEncoder passwordEncoder) {
|
||||
}
|
||||
|
||||
protected final ObjectMapper getObjectMapper() {
|
||||
return this.objectMapper;
|
||||
}
|
||||
|
||||
private String writeMap(Map<String, Object> data) {
|
||||
try {
|
||||
return this.objectMapper.writeValueAsString(data);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -90,10 +90,11 @@ public class RegisteredClient implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the client secret.
|
||||
* Returns the client secret or {@code null} if not available.
|
||||
*
|
||||
* @return the client secret
|
||||
* @return the client secret or {@code null} if not available
|
||||
*/
|
||||
@Nullable
|
||||
public String getClientSecret() {
|
||||
return this.clientSecret;
|
||||
}
|
||||
@@ -190,15 +191,15 @@ public class RegisteredClient implements Serializable {
|
||||
Objects.equals(this.authorizationGrantTypes, that.authorizationGrantTypes) &&
|
||||
Objects.equals(this.redirectUris, that.redirectUris) &&
|
||||
Objects.equals(this.scopes, that.scopes) &&
|
||||
Objects.equals(this.clientSettings.settings(), that.getClientSettings().settings()) &&
|
||||
Objects.equals(this.tokenSettings.settings(), that.tokenSettings.settings());
|
||||
Objects.equals(this.clientSettings, that.clientSettings) &&
|
||||
Objects.equals(this.tokenSettings, that.tokenSettings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.id, this.clientId, this.clientIdIssuedAt, this.clientSecret, this.clientSecretExpiresAt,
|
||||
this.clientName, this.clientAuthenticationMethods, this.authorizationGrantTypes, this.redirectUris,
|
||||
this.scopes, this.clientSettings.settings(), this.tokenSettings.settings());
|
||||
this.scopes, this.clientSettings, this.tokenSettings);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -211,8 +212,8 @@ public class RegisteredClient implements Serializable {
|
||||
", authorizationGrantTypes=" + this.authorizationGrantTypes +
|
||||
", redirectUris=" + this.redirectUris +
|
||||
", scopes=" + this.scopes +
|
||||
", clientSettings=" + this.clientSettings.settings() +
|
||||
", tokenSettings=" + this.tokenSettings.settings() +
|
||||
", clientSettings=" + this.clientSettings +
|
||||
", tokenSettings=" + this.tokenSettings +
|
||||
'}';
|
||||
}
|
||||
|
||||
@@ -249,38 +250,38 @@ public class RegisteredClient implements Serializable {
|
||||
private String clientSecret;
|
||||
private Instant clientSecretExpiresAt;
|
||||
private String clientName;
|
||||
private Set<ClientAuthenticationMethod> clientAuthenticationMethods = new HashSet<>();
|
||||
private Set<AuthorizationGrantType> authorizationGrantTypes = new HashSet<>();
|
||||
private Set<String> redirectUris = new HashSet<>();
|
||||
private Set<String> scopes = new HashSet<>();
|
||||
private ClientSettings clientSettings = new ClientSettings();
|
||||
private TokenSettings tokenSettings = new TokenSettings();
|
||||
private final Set<ClientAuthenticationMethod> clientAuthenticationMethods = new HashSet<>();
|
||||
private final Set<AuthorizationGrantType> authorizationGrantTypes = new HashSet<>();
|
||||
private final Set<String> redirectUris = new HashSet<>();
|
||||
private final Set<String> scopes = new HashSet<>();
|
||||
private ClientSettings clientSettings;
|
||||
private TokenSettings tokenSettings;
|
||||
|
||||
protected Builder(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
protected Builder(RegisteredClient registeredClient) {
|
||||
this.id = registeredClient.id;
|
||||
this.clientId = registeredClient.clientId;
|
||||
this.clientIdIssuedAt = registeredClient.clientIdIssuedAt;
|
||||
this.clientSecret = registeredClient.clientSecret;
|
||||
this.clientSecretExpiresAt = registeredClient.clientSecretExpiresAt;
|
||||
this.clientName = registeredClient.clientName;
|
||||
if (!CollectionUtils.isEmpty(registeredClient.clientAuthenticationMethods)) {
|
||||
this.clientAuthenticationMethods.addAll(registeredClient.clientAuthenticationMethods);
|
||||
this.id = registeredClient.getId();
|
||||
this.clientId = registeredClient.getClientId();
|
||||
this.clientIdIssuedAt = registeredClient.getClientIdIssuedAt();
|
||||
this.clientSecret = registeredClient.getClientSecret();
|
||||
this.clientSecretExpiresAt = registeredClient.getClientSecretExpiresAt();
|
||||
this.clientName = registeredClient.getClientName();
|
||||
if (!CollectionUtils.isEmpty(registeredClient.getClientAuthenticationMethods())) {
|
||||
this.clientAuthenticationMethods.addAll(registeredClient.getClientAuthenticationMethods());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(registeredClient.authorizationGrantTypes)) {
|
||||
this.authorizationGrantTypes.addAll(registeredClient.authorizationGrantTypes);
|
||||
if (!CollectionUtils.isEmpty(registeredClient.getAuthorizationGrantTypes())) {
|
||||
this.authorizationGrantTypes.addAll(registeredClient.getAuthorizationGrantTypes());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(registeredClient.redirectUris)) {
|
||||
this.redirectUris.addAll(registeredClient.redirectUris);
|
||||
if (!CollectionUtils.isEmpty(registeredClient.getRedirectUris())) {
|
||||
this.redirectUris.addAll(registeredClient.getRedirectUris());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(registeredClient.scopes)) {
|
||||
this.scopes.addAll(registeredClient.scopes);
|
||||
if (!CollectionUtils.isEmpty(registeredClient.getScopes())) {
|
||||
this.scopes.addAll(registeredClient.getScopes());
|
||||
}
|
||||
this.clientSettings = new ClientSettings(registeredClient.clientSettings.settings());
|
||||
this.tokenSettings = new TokenSettings(registeredClient.tokenSettings.settings());
|
||||
this.clientSettings = ClientSettings.withSettings(registeredClient.getClientSettings().getSettings()).build();
|
||||
this.tokenSettings = TokenSettings.withSettings(registeredClient.getTokenSettings().getSettings()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -444,26 +445,24 @@ public class RegisteredClient implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Consumer} of the client configuration settings,
|
||||
* allowing the ability to add, replace, or remove.
|
||||
* Sets the {@link ClientSettings client configuration settings}.
|
||||
*
|
||||
* @param clientSettingsConsumer a {@link Consumer} of the client configuration settings
|
||||
* @param clientSettings the client configuration settings
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder clientSettings(Consumer<ClientSettings> clientSettingsConsumer) {
|
||||
clientSettingsConsumer.accept(this.clientSettings);
|
||||
public Builder clientSettings(ClientSettings clientSettings) {
|
||||
this.clientSettings = clientSettings;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Consumer} of the token configuration settings,
|
||||
* allowing the ability to add, replace, or remove.
|
||||
* Sets the {@link TokenSettings token configuration settings}.
|
||||
*
|
||||
* @param tokenSettingsConsumer a {@link Consumer} of the token configuration settings
|
||||
* @param tokenSettings the token configuration settings
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder tokenSettings(Consumer<TokenSettings> tokenSettingsConsumer) {
|
||||
tokenSettingsConsumer.accept(this.tokenSettings);
|
||||
public Builder tokenSettings(TokenSettings tokenSettings) {
|
||||
this.tokenSettings = tokenSettings;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -482,13 +481,33 @@ public class RegisteredClient implements Serializable {
|
||||
this.clientName = this.id;
|
||||
}
|
||||
if (CollectionUtils.isEmpty(this.clientAuthenticationMethods)) {
|
||||
this.clientAuthenticationMethods.add(ClientAuthenticationMethod.BASIC);
|
||||
this.clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
}
|
||||
if (this.clientSettings == null) {
|
||||
ClientSettings.Builder builder = ClientSettings.builder();
|
||||
if (isPublicClientType()) {
|
||||
// @formatter:off
|
||||
builder
|
||||
.requireProofKey(true)
|
||||
.requireAuthorizationConsent(true);
|
||||
// @formatter:on
|
||||
}
|
||||
this.clientSettings = builder.build();
|
||||
}
|
||||
if (this.tokenSettings == null) {
|
||||
this.tokenSettings = TokenSettings.builder().build();
|
||||
}
|
||||
validateScopes();
|
||||
validateRedirectUris();
|
||||
return create();
|
||||
}
|
||||
|
||||
private boolean isPublicClientType() {
|
||||
return this.authorizationGrantTypes.contains(AuthorizationGrantType.AUTHORIZATION_CODE) &&
|
||||
this.clientAuthenticationMethods.size() == 1 &&
|
||||
this.clientAuthenticationMethods.contains(ClientAuthenticationMethod.NONE);
|
||||
}
|
||||
|
||||
private RegisteredClient create() {
|
||||
RegisteredClient registeredClient = new RegisteredClient();
|
||||
|
||||
@@ -506,8 +525,8 @@ public class RegisteredClient implements Serializable {
|
||||
new HashSet<>(this.redirectUris));
|
||||
registeredClient.scopes = Collections.unmodifiableSet(
|
||||
new HashSet<>(this.scopes));
|
||||
registeredClient.clientSettings = new ClientSettings(this.clientSettings.settings());
|
||||
registeredClient.tokenSettings = new TokenSettings(this.tokenSettings.settings());
|
||||
registeredClient.clientSettings = this.clientSettings;
|
||||
registeredClient.tokenSettings = this.tokenSettings;
|
||||
|
||||
return registeredClient;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ public interface RegisteredClientRepository {
|
||||
/**
|
||||
* Saves the registered client.
|
||||
*
|
||||
* <p>
|
||||
* IMPORTANT: Sensitive information should be encoded externally from the implementation, e.g. {@link RegisteredClient#getClientSecret()}
|
||||
*
|
||||
* @param registeredClient the {@link RegisteredClient}
|
||||
*/
|
||||
void save(RegisteredClient registeredClient);
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Base implementation for configuration settings.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.2
|
||||
*/
|
||||
public abstract class AbstractSettings implements Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private final Map<String, Object> settings;
|
||||
|
||||
protected AbstractSettings(Map<String, Object> settings) {
|
||||
Assert.notEmpty(settings, "settings cannot be empty");
|
||||
this.settings = Collections.unmodifiableMap(new HashMap<>(settings));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a configuration setting.
|
||||
*
|
||||
* @param name the name of the setting
|
||||
* @param <T> the type of the setting
|
||||
* @return the value of the setting, or {@code null} if not available
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getSetting(String name) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
return (T) getSettings().get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code Map} of the configuration settings.
|
||||
*
|
||||
* @return a {@code Map} of the configuration settings
|
||||
*/
|
||||
public Map<String, Object> getSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
AbstractSettings that = (AbstractSettings) obj;
|
||||
return this.settings.equals(that.settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AbstractSettings {" +
|
||||
"settings=" + this.settings +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for subclasses of {@link AbstractSettings}.
|
||||
*/
|
||||
protected static abstract class AbstractBuilder<T extends AbstractSettings, B extends AbstractBuilder<T, B>> {
|
||||
private final Map<String, Object> settings = new HashMap<>();
|
||||
|
||||
protected AbstractBuilder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a configuration setting.
|
||||
*
|
||||
* @param name the name of the setting
|
||||
* @param value the value of the setting
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B setting(String name, Object value) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
getSettings().put(name, value);
|
||||
return getThis();
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the configuration settings {@code Map}
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param settingsConsumer a {@link Consumer} of the configuration settings {@code Map}
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
*/
|
||||
public B settings(Consumer<Map<String, Object>> settingsConsumer) {
|
||||
settingsConsumer.accept(getSettings());
|
||||
return getThis();
|
||||
}
|
||||
|
||||
public abstract T build();
|
||||
|
||||
protected final Map<String, Object> getSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected final B getThis() {
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,34 +15,24 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A facility for client configuration settings.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.2
|
||||
* @see Settings
|
||||
* @see AbstractSettings
|
||||
* @see ConfigurationSettingNames.Client
|
||||
*/
|
||||
public class ClientSettings extends Settings {
|
||||
private static final String CLIENT_SETTING_BASE = "setting.client.";
|
||||
public static final String REQUIRE_PROOF_KEY = CLIENT_SETTING_BASE.concat("require-proof-key");
|
||||
public static final String REQUIRE_USER_CONSENT = CLIENT_SETTING_BASE.concat("require-user-consent");
|
||||
public final class ClientSettings extends AbstractSettings {
|
||||
|
||||
/**
|
||||
* Constructs a {@code ClientSettings}.
|
||||
*/
|
||||
public ClientSettings() {
|
||||
this(defaultSettings());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code ClientSettings} using the provided parameters.
|
||||
*
|
||||
* @param settings the initial settings
|
||||
*/
|
||||
public ClientSettings(Map<String, Object> settings) {
|
||||
private ClientSettings(Map<String, Object> settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@@ -52,48 +42,130 @@ public class ClientSettings extends Settings {
|
||||
*
|
||||
* @return {@code true} if the client is required to provide a proof key challenge and verifier, {@code false} otherwise
|
||||
*/
|
||||
public boolean requireProofKey() {
|
||||
return setting(REQUIRE_PROOF_KEY);
|
||||
public boolean isRequireProofKey() {
|
||||
return getSetting(ConfigurationSettingNames.Client.REQUIRE_PROOF_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if the client is required to provide a proof key challenge and verifier
|
||||
* when performing the Authorization Code Grant flow.
|
||||
*
|
||||
* @param requireProofKey {@code true} if the client is required to provide a proof key challenge and verifier, {@code false} otherwise
|
||||
* @return the {@link ClientSettings}
|
||||
*/
|
||||
public ClientSettings requireProofKey(boolean requireProofKey) {
|
||||
setting(REQUIRE_PROOF_KEY, requireProofKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the user's consent is required when the client requests access.
|
||||
* Returns {@code true} if authorization consent is required when the client requests access.
|
||||
* The default is {@code false}.
|
||||
*
|
||||
* @return {@code true} if the user's consent is required when the client requests access, {@code false} otherwise
|
||||
* @return {@code true} if authorization consent is required when the client requests access, {@code false} otherwise
|
||||
*/
|
||||
public boolean requireUserConsent() {
|
||||
return setting(REQUIRE_USER_CONSENT);
|
||||
public boolean isRequireAuthorizationConsent() {
|
||||
return getSetting(ConfigurationSettingNames.Client.REQUIRE_AUTHORIZATION_CONSENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if the user's consent is required when the client requests access.
|
||||
* This applies to all interactive flows (e.g. {@code authorization_code} and {@code device_code}).
|
||||
* Returns the {@code URL} for the Client's JSON Web Key Set.
|
||||
*
|
||||
* @param requireUserConsent {@code true} if the user's consent is required when the client requests access, {@code false} otherwise
|
||||
* @return the {@link ClientSettings}
|
||||
* @return the {@code URL} for the Client's JSON Web Key Set
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public ClientSettings requireUserConsent(boolean requireUserConsent) {
|
||||
setting(REQUIRE_USER_CONSENT, requireUserConsent);
|
||||
return this;
|
||||
public String getJwkSetUrl() {
|
||||
return getSetting(ConfigurationSettingNames.Client.JWK_SET_URL);
|
||||
}
|
||||
|
||||
protected static Map<String, Object> defaultSettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put(REQUIRE_PROOF_KEY, false);
|
||||
settings.put(REQUIRE_USER_CONSENT, false);
|
||||
return settings;
|
||||
/**
|
||||
* Returns the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
|
||||
* the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
|
||||
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods.
|
||||
*
|
||||
* @return the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate the Client at the Token Endpoint
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public JwsAlgorithm getTokenEndpointAuthenticationSigningAlgorithm() {
|
||||
return getSetting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the default settings.
|
||||
*
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder()
|
||||
.requireProofKey(false)
|
||||
.requireAuthorizationConsent(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided settings.
|
||||
*
|
||||
* @param settings the settings to initialize the builder
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withSettings(Map<String, Object> settings) {
|
||||
Assert.notEmpty(settings, "settings cannot be empty");
|
||||
return new Builder()
|
||||
.settings(s -> s.putAll(settings));
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link ClientSettings}.
|
||||
*/
|
||||
public static class Builder extends AbstractBuilder<ClientSettings, Builder> {
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if the client is required to provide a proof key challenge and verifier
|
||||
* when performing the Authorization Code Grant flow.
|
||||
*
|
||||
* @param requireProofKey {@code true} if the client is required to provide a proof key challenge and verifier, {@code false} otherwise
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder requireProofKey(boolean requireProofKey) {
|
||||
return setting(ConfigurationSettingNames.Client.REQUIRE_PROOF_KEY, requireProofKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if authorization consent is required when the client requests access.
|
||||
* This applies to all interactive flows (e.g. {@code authorization_code} and {@code device_code}).
|
||||
*
|
||||
* @param requireAuthorizationConsent {@code true} if authorization consent is required when the client requests access, {@code false} otherwise
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder requireAuthorizationConsent(boolean requireAuthorizationConsent) {
|
||||
return setting(ConfigurationSettingNames.Client.REQUIRE_AUTHORIZATION_CONSENT, requireAuthorizationConsent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code URL} for the Client's JSON Web Key Set.
|
||||
*
|
||||
* @param jwkSetUrl the {@code URL} for the Client's JSON Web Key Set
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public Builder jwkSetUrl(String jwkSetUrl) {
|
||||
return setting(ConfigurationSettingNames.Client.JWK_SET_URL, jwkSetUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
|
||||
* the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
|
||||
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods.
|
||||
|
||||
* @param authenticationSigningAlgorithm the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
|
||||
* used to authenticate the Client at the Token Endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public Builder tokenEndpointAuthenticationSigningAlgorithm(JwsAlgorithm authenticationSigningAlgorithm) {
|
||||
return setting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM, authenticationSigningAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link ClientSettings}.
|
||||
*
|
||||
* @return the {@link ClientSettings}
|
||||
*/
|
||||
@Override
|
||||
public ClientSettings build() {
|
||||
return new ClientSettings(getSettings());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.config;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
/**
|
||||
* The names for all the configuration settings.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.0
|
||||
*/
|
||||
public final class ConfigurationSettingNames {
|
||||
private static final String SETTINGS_NAMESPACE = "settings.";
|
||||
|
||||
private ConfigurationSettingNames() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The names for client configuration settings.
|
||||
*/
|
||||
public static final class Client {
|
||||
private static final String CLIENT_SETTINGS_NAMESPACE = SETTINGS_NAMESPACE.concat("client.");
|
||||
|
||||
/**
|
||||
* Set to {@code true} if the client is required to provide a proof key challenge and verifier
|
||||
* when performing the Authorization Code Grant flow.
|
||||
*/
|
||||
public static final String REQUIRE_PROOF_KEY = CLIENT_SETTINGS_NAMESPACE.concat("require-proof-key");
|
||||
|
||||
/**
|
||||
* Set to {@code true} if authorization consent is required when the client requests access.
|
||||
* This applies to all interactive flows (e.g. {@code authorization_code} and {@code device_code}).
|
||||
*/
|
||||
public static final String REQUIRE_AUTHORIZATION_CONSENT = CLIENT_SETTINGS_NAMESPACE.concat("require-authorization-consent");
|
||||
|
||||
/**
|
||||
* Set the {@code URL} for the Client's JSON Web Key Set.
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public static final String JWK_SET_URL = CLIENT_SETTINGS_NAMESPACE.concat("jwk-set-url");
|
||||
|
||||
/**
|
||||
* Set the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
|
||||
* used to authenticate the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
|
||||
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods.
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public static final String TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM = CLIENT_SETTINGS_NAMESPACE.concat("token-endpoint-authentication-signing-algorithm");
|
||||
|
||||
private Client() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The names for provider configuration settings.
|
||||
*/
|
||||
public static final class Provider {
|
||||
private static final String PROVIDER_SETTINGS_NAMESPACE = SETTINGS_NAMESPACE.concat("provider.");
|
||||
|
||||
/**
|
||||
* Set the URL the Provider uses as its Issuer Identifier.
|
||||
*/
|
||||
public static final String ISSUER = PROVIDER_SETTINGS_NAMESPACE.concat("issuer");
|
||||
|
||||
/**
|
||||
* Set the Provider's OAuth 2.0 Authorization endpoint.
|
||||
*/
|
||||
public static final String AUTHORIZATION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("authorization-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's OAuth 2.0 Token endpoint.
|
||||
*/
|
||||
public static final String TOKEN_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("token-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's JWK Set endpoint.
|
||||
*/
|
||||
public static final String JWK_SET_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("jwk-set-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's OAuth 2.0 Token Revocation endpoint.
|
||||
*/
|
||||
public static final String TOKEN_REVOCATION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("token-revocation-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's OAuth 2.0 Token Introspection endpoint.
|
||||
*/
|
||||
public static final String TOKEN_INTROSPECTION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("token-introspection-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's OpenID Connect 1.0 Client Registration endpoint.
|
||||
*/
|
||||
public static final String OIDC_CLIENT_REGISTRATION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("oidc-client-registration-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's OpenID Connect 1.0 UserInfo endpoint.
|
||||
*/
|
||||
public static final String OIDC_USER_INFO_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("oidc-user-info-endpoint");
|
||||
|
||||
private Provider() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The names for token configuration settings.
|
||||
*/
|
||||
public static final class Token {
|
||||
private static final String TOKEN_SETTINGS_NAMESPACE = SETTINGS_NAMESPACE.concat("token.");
|
||||
|
||||
/**
|
||||
* Set the time-to-live for an access token.
|
||||
*/
|
||||
public static final String ACCESS_TOKEN_TIME_TO_LIVE = TOKEN_SETTINGS_NAMESPACE.concat("access-token-time-to-live");
|
||||
|
||||
/**
|
||||
* Set the {@link OAuth2TokenFormat token format} for an access token.
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public static final String ACCESS_TOKEN_FORMAT = TOKEN_SETTINGS_NAMESPACE.concat("access-token-format");
|
||||
|
||||
/**
|
||||
* Set to {@code true} if refresh tokens are reused when returning the access token response,
|
||||
* or {@code false} if a new refresh token is issued.
|
||||
*/
|
||||
public static final String REUSE_REFRESH_TOKENS = TOKEN_SETTINGS_NAMESPACE.concat("reuse-refresh-tokens");
|
||||
|
||||
/**
|
||||
* Set the time-to-live for a refresh token.
|
||||
*/
|
||||
public static final String REFRESH_TOKEN_TIME_TO_LIVE = TOKEN_SETTINGS_NAMESPACE.concat("refresh-token-time-to-live");
|
||||
|
||||
/**
|
||||
* Set the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}.
|
||||
*/
|
||||
public static final String ID_TOKEN_SIGNATURE_ALGORITHM = TOKEN_SETTINGS_NAMESPACE.concat("id-token-signature-algorithm");
|
||||
|
||||
private Token() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,39 +15,22 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A facility for provider configuration settings.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see Settings
|
||||
* @see AbstractSettings
|
||||
* @see ConfigurationSettingNames.Provider
|
||||
*/
|
||||
public class ProviderSettings extends Settings {
|
||||
private static final String PROVIDER_SETTING_BASE = "setting.provider.";
|
||||
public static final String ISSUER = PROVIDER_SETTING_BASE.concat("issuer");
|
||||
public static final String AUTHORIZATION_ENDPOINT = PROVIDER_SETTING_BASE.concat("authorization-endpoint");
|
||||
public static final String TOKEN_ENDPOINT = PROVIDER_SETTING_BASE.concat("token-endpoint");
|
||||
public static final String JWK_SET_ENDPOINT = PROVIDER_SETTING_BASE.concat("jwk-set-endpoint");
|
||||
public static final String TOKEN_REVOCATION_ENDPOINT = PROVIDER_SETTING_BASE.concat("token-revocation-endpoint");
|
||||
public static final String TOKEN_INTROSPECTION_ENDPOINT = PROVIDER_SETTING_BASE.concat("token-introspection-endpoint");
|
||||
public static final String OIDC_CLIENT_REGISTRATION_ENDPOINT = PROVIDER_SETTING_BASE.concat("oidc-client-registration-endpoint");
|
||||
public final class ProviderSettings extends AbstractSettings {
|
||||
|
||||
/**
|
||||
* Constructs a {@code ProviderSettings}.
|
||||
*/
|
||||
public ProviderSettings() {
|
||||
this(defaultSettings());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code ProviderSettings} using the provided parameters.
|
||||
*
|
||||
* @param settings the initial settings
|
||||
*/
|
||||
public ProviderSettings(Map<String, Object> settings) {
|
||||
private ProviderSettings(Map<String, Object> settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@@ -56,18 +39,8 @@ public class ProviderSettings extends Settings {
|
||||
*
|
||||
* @return the URL of the Provider's Issuer Identifier
|
||||
*/
|
||||
public String issuer() {
|
||||
return setting(ISSUER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL the Provider uses as its Issuer Identifier.
|
||||
*
|
||||
* @param issuer the URL the Provider uses as its Issuer Identifier.
|
||||
* @return the {@link ProviderSettings} for further configuration
|
||||
*/
|
||||
public ProviderSettings issuer(String issuer) {
|
||||
return setting(ISSUER, issuer);
|
||||
public String getIssuer() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.ISSUER);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,18 +48,8 @@ public class ProviderSettings extends Settings {
|
||||
*
|
||||
* @return the Authorization endpoint
|
||||
*/
|
||||
public String authorizationEndpoint() {
|
||||
return setting(AUTHORIZATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Authorization endpoint.
|
||||
*
|
||||
* @param authorizationEndpoint the Authorization endpoint
|
||||
* @return the {@link ProviderSettings} for further configuration
|
||||
*/
|
||||
public ProviderSettings authorizationEndpoint(String authorizationEndpoint) {
|
||||
return setting(AUTHORIZATION_ENDPOINT, authorizationEndpoint);
|
||||
public String getAuthorizationEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.AUTHORIZATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,18 +57,8 @@ public class ProviderSettings extends Settings {
|
||||
*
|
||||
* @return the Token endpoint
|
||||
*/
|
||||
public String tokenEndpoint() {
|
||||
return setting(TOKEN_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Token endpoint.
|
||||
*
|
||||
* @param tokenEndpoint the Token endpoint
|
||||
* @return the {@link ProviderSettings} for further configuration
|
||||
*/
|
||||
public ProviderSettings tokenEndpoint(String tokenEndpoint) {
|
||||
return setting(TOKEN_ENDPOINT, tokenEndpoint);
|
||||
public String getTokenEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.TOKEN_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,18 +66,8 @@ public class ProviderSettings extends Settings {
|
||||
*
|
||||
* @return the JWK Set endpoint
|
||||
*/
|
||||
public String jwkSetEndpoint() {
|
||||
return setting(JWK_SET_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's JWK Set endpoint.
|
||||
*
|
||||
* @param jwkSetEndpoint the JWK Set endpoint
|
||||
* @return the {@link ProviderSettings} for further configuration
|
||||
*/
|
||||
public ProviderSettings jwkSetEndpoint(String jwkSetEndpoint) {
|
||||
return setting(JWK_SET_ENDPOINT, jwkSetEndpoint);
|
||||
public String getJwkSetEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,18 +75,8 @@ public class ProviderSettings extends Settings {
|
||||
*
|
||||
* @return the Token Revocation endpoint
|
||||
*/
|
||||
public String tokenRevocationEndpoint() {
|
||||
return setting(TOKEN_REVOCATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Token Revocation endpoint.
|
||||
*
|
||||
* @param tokenRevocationEndpoint the Token Revocation endpoint
|
||||
* @return the {@link ProviderSettings} for further configuration
|
||||
*/
|
||||
public ProviderSettings tokenRevocationEndpoint(String tokenRevocationEndpoint) {
|
||||
return setting(TOKEN_REVOCATION_ENDPOINT, tokenRevocationEndpoint);
|
||||
public String getTokenRevocationEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.TOKEN_REVOCATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,18 +84,8 @@ public class ProviderSettings extends Settings {
|
||||
*
|
||||
* @return the Token Introspection endpoint
|
||||
*/
|
||||
public String tokenIntrospectionEndpoint() {
|
||||
return setting(TOKEN_INTROSPECTION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Token Introspection endpoint.
|
||||
*
|
||||
* @param tokenIntrospectionEndpoint the Token Introspection endpoint
|
||||
* @return the {@link ProviderSettings} for further configuration
|
||||
*/
|
||||
public ProviderSettings tokenIntrospectionEndpoint(String tokenIntrospectionEndpoint) {
|
||||
return setting(TOKEN_INTROSPECTION_ENDPOINT, tokenIntrospectionEndpoint);
|
||||
public String getTokenIntrospectionEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.TOKEN_INTROSPECTION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,28 +93,145 @@ public class ProviderSettings extends Settings {
|
||||
*
|
||||
* @return the OpenID Connect 1.0 Client Registration endpoint
|
||||
*/
|
||||
public String oidcClientRegistrationEndpoint() {
|
||||
return setting(OIDC_CLIENT_REGISTRATION_ENDPOINT);
|
||||
public String getOidcClientRegistrationEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.OIDC_CLIENT_REGISTRATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OpenID Connect 1.0 Client Registration endpoint.
|
||||
* Returns the Provider's OpenID Connect 1.0 UserInfo endpoint. The default is {@code /userinfo}.
|
||||
*
|
||||
* @param oidcClientRegistrationEndpoint the OpenID Connect 1.0 Client Registration endpoint
|
||||
* @return the {@link ProviderSettings} for further configuration
|
||||
* @return the OpenID Connect 1.0 UserInfo endpoint
|
||||
*/
|
||||
public ProviderSettings oidcClientRegistrationEndpoint(String oidcClientRegistrationEndpoint) {
|
||||
return setting(OIDC_CLIENT_REGISTRATION_ENDPOINT, oidcClientRegistrationEndpoint);
|
||||
public String getOidcUserInfoEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.OIDC_USER_INFO_ENDPOINT);
|
||||
}
|
||||
|
||||
protected static Map<String, Object> defaultSettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put(AUTHORIZATION_ENDPOINT, "/oauth2/authorize");
|
||||
settings.put(TOKEN_ENDPOINT, "/oauth2/token");
|
||||
settings.put(JWK_SET_ENDPOINT, "/oauth2/jwks");
|
||||
settings.put(TOKEN_REVOCATION_ENDPOINT, "/oauth2/revoke");
|
||||
settings.put(TOKEN_INTROSPECTION_ENDPOINT, "/oauth2/introspect");
|
||||
settings.put(OIDC_CLIENT_REGISTRATION_ENDPOINT, "/connect/register");
|
||||
return settings;
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the default settings.
|
||||
*
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder()
|
||||
.authorizationEndpoint("/oauth2/authorize")
|
||||
.tokenEndpoint("/oauth2/token")
|
||||
.jwkSetEndpoint("/oauth2/jwks")
|
||||
.tokenRevocationEndpoint("/oauth2/revoke")
|
||||
.tokenIntrospectionEndpoint("/oauth2/introspect")
|
||||
.oidcClientRegistrationEndpoint("/connect/register")
|
||||
.oidcUserInfoEndpoint("/userinfo");
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided settings.
|
||||
*
|
||||
* @param settings the settings to initialize the builder
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withSettings(Map<String, Object> settings) {
|
||||
Assert.notEmpty(settings, "settings cannot be empty");
|
||||
return new Builder()
|
||||
.settings(s -> s.putAll(settings));
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link ProviderSettings}.
|
||||
*/
|
||||
public static class Builder extends AbstractBuilder<ProviderSettings, Builder> {
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL the Provider uses as its Issuer Identifier.
|
||||
*
|
||||
* @param issuer the URL the Provider uses as its Issuer Identifier.
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder issuer(String issuer) {
|
||||
return setting(ConfigurationSettingNames.Provider.ISSUER, issuer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Authorization endpoint.
|
||||
*
|
||||
* @param authorizationEndpoint the Authorization endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder authorizationEndpoint(String authorizationEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Token endpoint.
|
||||
*
|
||||
* @param tokenEndpoint the Token endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder tokenEndpoint(String tokenEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.TOKEN_ENDPOINT, tokenEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's JWK Set endpoint.
|
||||
*
|
||||
* @param jwkSetEndpoint the JWK Set endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder jwkSetEndpoint(String jwkSetEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT, jwkSetEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Token Revocation endpoint.
|
||||
*
|
||||
* @param tokenRevocationEndpoint the Token Revocation endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder tokenRevocationEndpoint(String tokenRevocationEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.TOKEN_REVOCATION_ENDPOINT, tokenRevocationEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Token Introspection endpoint.
|
||||
*
|
||||
* @param tokenIntrospectionEndpoint the Token Introspection endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder tokenIntrospectionEndpoint(String tokenIntrospectionEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.TOKEN_INTROSPECTION_ENDPOINT, tokenIntrospectionEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OpenID Connect 1.0 Client Registration endpoint.
|
||||
*
|
||||
* @param oidcClientRegistrationEndpoint the OpenID Connect 1.0 Client Registration endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder oidcClientRegistrationEndpoint(String oidcClientRegistrationEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.OIDC_CLIENT_REGISTRATION_ENDPOINT, oidcClientRegistrationEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OpenID Connect 1.0 UserInfo endpoint.
|
||||
*
|
||||
* @param oidcUserInfoEndpoint the OpenID Connect 1.0 UserInfo endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder oidcUserInfoEndpoint(String oidcUserInfoEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.OIDC_USER_INFO_ENDPOINT, oidcUserInfoEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link ProviderSettings}.
|
||||
*
|
||||
* @return the {@link ProviderSettings}
|
||||
*/
|
||||
@Override
|
||||
public ProviderSettings build() {
|
||||
return new ProviderSettings(getSettings());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* 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.oauth2.server.authorization.config;
|
||||
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A facility for configuration settings.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.2
|
||||
*/
|
||||
public class Settings implements Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private final Map<String, Object> settings;
|
||||
|
||||
/**
|
||||
* Constructs a {@code Settings}.
|
||||
*/
|
||||
public Settings() {
|
||||
this.settings = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code Settings} using the provided parameters.
|
||||
*
|
||||
* @param settings the initial settings
|
||||
*/
|
||||
public Settings(Map<String, Object> settings) {
|
||||
Assert.notNull(settings, "settings cannot be null");
|
||||
this.settings = new HashMap<>(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a configuration setting.
|
||||
*
|
||||
* @param name the name of the setting
|
||||
* @param <T> the type of the setting
|
||||
* @return the value of the setting, or {@code null} if not available
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T setting(String name) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
return (T) this.settings.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a configuration setting.
|
||||
*
|
||||
* @param name the name of the setting
|
||||
* @param value the value of the setting
|
||||
* @param <T> the type of the {@link Settings}
|
||||
* @return the {@link Settings}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Settings> T setting(String name, Object value) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
this.settings.put(name, value);
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code Map} of the configuration settings.
|
||||
*
|
||||
* @return a {@code Map} of the configuration settings
|
||||
*/
|
||||
public Map<String, Object> settings() {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the configuration settings {@code Map}
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param settingsConsumer a {@link Consumer} of the configuration settings {@code Map}
|
||||
* @param <T> the type of the {@link Settings}
|
||||
* @return the {@link Settings}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Settings> T settings(Consumer<Map<String, Object>> settingsConsumer) {
|
||||
settingsConsumer.accept(this.settings);
|
||||
return (T) this;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,9 +16,9 @@
|
||||
package org.springframework.security.oauth2.server.authorization.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -28,28 +28,12 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.2
|
||||
* @see Settings
|
||||
* @see AbstractSettings
|
||||
* @see ConfigurationSettingNames.Token
|
||||
*/
|
||||
public class TokenSettings extends Settings {
|
||||
private static final String TOKEN_SETTING_BASE = "setting.token.";
|
||||
public static final String ACCESS_TOKEN_TIME_TO_LIVE = TOKEN_SETTING_BASE.concat("access-token-time-to-live");
|
||||
public static final String REUSE_REFRESH_TOKENS = TOKEN_SETTING_BASE.concat("reuse-refresh-tokens");
|
||||
public static final String REFRESH_TOKEN_TIME_TO_LIVE = TOKEN_SETTING_BASE.concat("refresh-token-time-to-live");
|
||||
public static final String ID_TOKEN_SIGNATURE_ALGORITHM = TOKEN_SETTING_BASE.concat("id-token-signature-algorithm");
|
||||
public final class TokenSettings extends AbstractSettings {
|
||||
|
||||
/**
|
||||
* Constructs a {@code TokenSettings}.
|
||||
*/
|
||||
public TokenSettings() {
|
||||
this(defaultSettings());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code TokenSettings} using the provided parameters.
|
||||
*
|
||||
* @param settings the initial settings
|
||||
*/
|
||||
public TokenSettings(Map<String, Object> settings) {
|
||||
private TokenSettings(Map<String, Object> settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@@ -58,41 +42,27 @@ public class TokenSettings extends Settings {
|
||||
*
|
||||
* @return the time-to-live for an access token
|
||||
*/
|
||||
public Duration accessTokenTimeToLive() {
|
||||
return setting(ACCESS_TOKEN_TIME_TO_LIVE);
|
||||
public Duration getAccessTokenTimeToLive() {
|
||||
return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time-to-live for an access token. Must be greater than {@code Duration.ZERO}.
|
||||
* Returns the token format for an access token.
|
||||
* The default is {@link OAuth2TokenFormat#SELF_CONTAINED}.
|
||||
*
|
||||
* @param accessTokenTimeToLive the time-to-live for an access token
|
||||
* @return the {@link TokenSettings}
|
||||
* @return the token format for an access token
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public TokenSettings accessTokenTimeToLive(Duration accessTokenTimeToLive) {
|
||||
Assert.notNull(accessTokenTimeToLive, "accessTokenTimeToLive cannot be null");
|
||||
Assert.isTrue(accessTokenTimeToLive.getSeconds() > 0, "accessTokenTimeToLive must be greater than Duration.ZERO");
|
||||
setting(ACCESS_TOKEN_TIME_TO_LIVE, accessTokenTimeToLive);
|
||||
return this;
|
||||
public OAuth2TokenFormat getAccessTokenFormat() {
|
||||
return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if refresh tokens are reused when returning the access token response,
|
||||
* or {@code false} if a new refresh token is issued. The default is {@code true}.
|
||||
*/
|
||||
public boolean reuseRefreshTokens() {
|
||||
return setting(REUSE_REFRESH_TOKENS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if refresh tokens are reused when returning the access token response,
|
||||
* or {@code false} if a new refresh token is issued.
|
||||
*
|
||||
* @param reuseRefreshTokens {@code true} to reuse refresh tokens, {@code false} to issue new refresh tokens
|
||||
* @return the {@link TokenSettings}
|
||||
*/
|
||||
public TokenSettings reuseRefreshTokens(boolean reuseRefreshTokens) {
|
||||
setting(REUSE_REFRESH_TOKENS, reuseRefreshTokens);
|
||||
return this;
|
||||
public boolean isReuseRefreshTokens() {
|
||||
return getSetting(ConfigurationSettingNames.Token.REUSE_REFRESH_TOKENS);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,21 +70,8 @@ public class TokenSettings extends Settings {
|
||||
*
|
||||
* @return the time-to-live for a refresh token
|
||||
*/
|
||||
public Duration refreshTokenTimeToLive() {
|
||||
return setting(REFRESH_TOKEN_TIME_TO_LIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time-to-live for a refresh token. Must be greater than {@code Duration.ZERO}.
|
||||
*
|
||||
* @param refreshTokenTimeToLive the time-to-live for a refresh token
|
||||
* @return the {@link TokenSettings}
|
||||
*/
|
||||
public TokenSettings refreshTokenTimeToLive(Duration refreshTokenTimeToLive) {
|
||||
Assert.notNull(refreshTokenTimeToLive, "refreshTokenTimeToLive cannot be null");
|
||||
Assert.isTrue(refreshTokenTimeToLive.getSeconds() > 0, "refreshTokenTimeToLive must be greater than Duration.ZERO");
|
||||
setting(REFRESH_TOKEN_TIME_TO_LIVE, refreshTokenTimeToLive);
|
||||
return this;
|
||||
public Duration getRefreshTokenTimeToLive() {
|
||||
return getSetting(ConfigurationSettingNames.Token.REFRESH_TOKEN_TIME_TO_LIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,29 +80,112 @@ public class TokenSettings extends Settings {
|
||||
*
|
||||
* @return the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}
|
||||
*/
|
||||
public SignatureAlgorithm idTokenSignatureAlgorithm() {
|
||||
return setting(ID_TOKEN_SIGNATURE_ALGORITHM);
|
||||
public SignatureAlgorithm getIdTokenSignatureAlgorithm() {
|
||||
return getSetting(ConfigurationSettingNames.Token.ID_TOKEN_SIGNATURE_ALGORITHM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}.
|
||||
* Constructs a new {@link Builder} with the default settings.
|
||||
*
|
||||
* @param idTokenSignatureAlgorithm the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}
|
||||
* @return the {@link TokenSettings}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public TokenSettings idTokenSignatureAlgorithm(SignatureAlgorithm idTokenSignatureAlgorithm) {
|
||||
Assert.notNull(idTokenSignatureAlgorithm, "idTokenSignatureAlgorithm cannot be null");
|
||||
setting(ID_TOKEN_SIGNATURE_ALGORITHM, idTokenSignatureAlgorithm);
|
||||
return this;
|
||||
public static Builder builder() {
|
||||
return new Builder()
|
||||
.accessTokenTimeToLive(Duration.ofMinutes(5))
|
||||
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
|
||||
.reuseRefreshTokens(true)
|
||||
.refreshTokenTimeToLive(Duration.ofMinutes(60))
|
||||
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256);
|
||||
}
|
||||
|
||||
protected static Map<String, Object> defaultSettings() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put(ACCESS_TOKEN_TIME_TO_LIVE, Duration.ofMinutes(5));
|
||||
settings.put(REUSE_REFRESH_TOKENS, true);
|
||||
settings.put(REFRESH_TOKEN_TIME_TO_LIVE, Duration.ofMinutes(60));
|
||||
settings.put(ID_TOKEN_SIGNATURE_ALGORITHM, SignatureAlgorithm.RS256);
|
||||
return settings;
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided settings.
|
||||
*
|
||||
* @param settings the settings to initialize the builder
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withSettings(Map<String, Object> settings) {
|
||||
Assert.notEmpty(settings, "settings cannot be empty");
|
||||
return new Builder()
|
||||
.settings(s -> s.putAll(settings));
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link TokenSettings}.
|
||||
*/
|
||||
public static class Builder extends AbstractBuilder<TokenSettings, Builder> {
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time-to-live for an access token. Must be greater than {@code Duration.ZERO}.
|
||||
*
|
||||
* @param accessTokenTimeToLive the time-to-live for an access token
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder accessTokenTimeToLive(Duration accessTokenTimeToLive) {
|
||||
Assert.notNull(accessTokenTimeToLive, "accessTokenTimeToLive cannot be null");
|
||||
Assert.isTrue(accessTokenTimeToLive.getSeconds() > 0, "accessTokenTimeToLive must be greater than Duration.ZERO");
|
||||
return setting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE, accessTokenTimeToLive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the token format for an access token.
|
||||
*
|
||||
* @param accessTokenFormat the token format for an access token
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public Builder accessTokenFormat(OAuth2TokenFormat accessTokenFormat) {
|
||||
Assert.notNull(accessTokenFormat, "accessTokenFormat cannot be null");
|
||||
return setting(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT, accessTokenFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if refresh tokens are reused when returning the access token response,
|
||||
* or {@code false} if a new refresh token is issued.
|
||||
*
|
||||
* @param reuseRefreshTokens {@code true} to reuse refresh tokens, {@code false} to issue new refresh tokens
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder reuseRefreshTokens(boolean reuseRefreshTokens) {
|
||||
return setting(ConfigurationSettingNames.Token.REUSE_REFRESH_TOKENS, reuseRefreshTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time-to-live for a refresh token. Must be greater than {@code Duration.ZERO}.
|
||||
*
|
||||
* @param refreshTokenTimeToLive the time-to-live for a refresh token
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder refreshTokenTimeToLive(Duration refreshTokenTimeToLive) {
|
||||
Assert.notNull(refreshTokenTimeToLive, "refreshTokenTimeToLive cannot be null");
|
||||
Assert.isTrue(refreshTokenTimeToLive.getSeconds() > 0, "refreshTokenTimeToLive must be greater than Duration.ZERO");
|
||||
return setting(ConfigurationSettingNames.Token.REFRESH_TOKEN_TIME_TO_LIVE, refreshTokenTimeToLive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}.
|
||||
*
|
||||
* @param idTokenSignatureAlgorithm the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder idTokenSignatureAlgorithm(SignatureAlgorithm idTokenSignatureAlgorithm) {
|
||||
Assert.notNull(idTokenSignatureAlgorithm, "idTokenSignatureAlgorithm cannot be null");
|
||||
return setting(ConfigurationSettingNames.Token.ID_TOKEN_SIGNATURE_ALGORITHM, idTokenSignatureAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link TokenSettings}.
|
||||
*
|
||||
* @return the {@link TokenSettings}
|
||||
*/
|
||||
@Override
|
||||
public TokenSettings build() {
|
||||
return new TokenSettings(getSettings());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.context;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A context that holds information of the Provider.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.2
|
||||
* @see ProviderSettings
|
||||
* @see ProviderContextHolder
|
||||
*/
|
||||
public final class ProviderContext {
|
||||
private final ProviderSettings providerSettings;
|
||||
private final Supplier<String> issuerSupplier;
|
||||
|
||||
/**
|
||||
* Constructs a {@code ProviderContext} using the provided parameters.
|
||||
*
|
||||
* @param providerSettings the provider settings
|
||||
* @param issuerSupplier a {@code Supplier} for the {@code URL} of the Provider's issuer identifier
|
||||
*/
|
||||
public ProviderContext(ProviderSettings providerSettings, @Nullable Supplier<String> issuerSupplier) {
|
||||
Assert.notNull(providerSettings, "providerSettings cannot be null");
|
||||
this.providerSettings = providerSettings;
|
||||
this.issuerSupplier = issuerSupplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ProviderSettings}.
|
||||
*
|
||||
* @return the {@link ProviderSettings}
|
||||
*/
|
||||
public ProviderSettings getProviderSettings() {
|
||||
return this.providerSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code URL} of the Provider's issuer identifier.
|
||||
* The issuer identifier is resolved from the constructor parameter {@code Supplier<String>}
|
||||
* or if not provided then defaults to {@link ProviderSettings#getIssuer()}.
|
||||
*
|
||||
* @return the {@code URL} of the Provider's issuer identifier
|
||||
*/
|
||||
public String getIssuer() {
|
||||
return this.issuerSupplier != null ?
|
||||
this.issuerSupplier.get() :
|
||||
getProviderSettings().getIssuer();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.context;
|
||||
|
||||
import org.springframework.security.oauth2.server.authorization.web.ProviderContextFilter;
|
||||
|
||||
/**
|
||||
* A holder of {@link ProviderContext} that associates it with the current thread using a {@code ThreadLocal}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.2
|
||||
* @see ProviderContext
|
||||
* @see ProviderContextFilter
|
||||
*/
|
||||
public final class ProviderContextHolder {
|
||||
private static final ThreadLocal<ProviderContext> holder = new ThreadLocal<>();
|
||||
|
||||
private ProviderContextHolder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ProviderContext} bound to the current thread.
|
||||
*
|
||||
* @return the {@link ProviderContext}
|
||||
*/
|
||||
public static ProviderContext getProviderContext() {
|
||||
return holder.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the given {@link ProviderContext} to the current thread.
|
||||
*
|
||||
* @param providerContext the {@link ProviderContext}
|
||||
*/
|
||||
public static void setProviderContext(ProviderContext providerContext) {
|
||||
if (providerContext == null) {
|
||||
resetProviderContext();
|
||||
} else {
|
||||
holder.set(providerContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the {@link ProviderContext} bound to the current thread.
|
||||
*/
|
||||
public static void resetProviderContext() {
|
||||
holder.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.jackson2;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
/**
|
||||
* This mixin class is used to serialize/deserialize {@link Duration}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
* @see Duration
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
creatorVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
abstract class DurationMixin {
|
||||
|
||||
@JsonCreator
|
||||
static void ofSeconds(@JsonProperty("seconds") long seconds, @JsonProperty("nano") long nanoAdjustment) {
|
||||
}
|
||||
|
||||
@JsonGetter("seconds")
|
||||
abstract long getSeconds();
|
||||
|
||||
@JsonGetter("nano")
|
||||
abstract int getNano();
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -13,20 +13,26 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.endpoint;
|
||||
package org.springframework.security.oauth2.server.authorization.jackson2;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA.
|
||||
* This mixin class is used to serialize/deserialize {@link HashSet}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.3
|
||||
* @see <a target="_blank" href="https://github.com/spring-projects/spring-security/issues/9183">Issue gh-9183</a>
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.1.2
|
||||
* @see HashSet
|
||||
*/
|
||||
public interface OAuth2ParameterNames2 extends OAuth2ParameterNames {
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
abstract class HashSetMixin {
|
||||
|
||||
String TOKEN = "token";
|
||||
|
||||
String TOKEN_TYPE_HINT = "token_type_hint";
|
||||
@JsonCreator
|
||||
HashSetMixin(Set<?> set) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.authorization.jackson2;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is a straight copy from Spring Security.
|
||||
* It should be consolidated when merging this codebase into Spring Security.
|
||||
*
|
||||
* Utility class for {@code JsonNode}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.3
|
||||
*/
|
||||
abstract class JsonNodeUtils {
|
||||
|
||||
static final TypeReference<Set<String>> STRING_SET = new TypeReference<Set<String>>() {
|
||||
};
|
||||
|
||||
static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<Map<String, Object>>() {
|
||||
};
|
||||
|
||||
static String findStringValue(JsonNode jsonNode, String fieldName) {
|
||||
if (jsonNode == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = jsonNode.findValue(fieldName);
|
||||
return (value != null && value.isTextual()) ? value.asText() : null;
|
||||
}
|
||||
|
||||
static <T> T findValue(JsonNode jsonNode, String fieldName, TypeReference<T> valueTypeReference,
|
||||
ObjectMapper mapper) {
|
||||
if (jsonNode == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = jsonNode.findValue(fieldName);
|
||||
return (value != null && value.isContainerNode()) ? mapper.convertValue(value, valueTypeReference) : null;
|
||||
}
|
||||
|
||||
static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) {
|
||||
if (jsonNode == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = jsonNode.findValue(fieldName);
|
||||
return (value != null && value.isObject()) ? value : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.jackson2;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
|
||||
/**
|
||||
* This mixin class is used to serialize/deserialize {@link SignatureAlgorithm}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
* @see SignatureAlgorithm
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
abstract class JwsAlgorithmMixin {
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.authorization.jackson2;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.util.StdConverter;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest.Builder;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is a straight copy from Spring Security.
|
||||
* It should be consolidated when merging this codebase into Spring Security.
|
||||
*
|
||||
* A {@code JsonDeserializer} for {@link OAuth2AuthorizationRequest}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.3
|
||||
* @see OAuth2AuthorizationRequest
|
||||
* @see OAuth2AuthorizationRequestMixin
|
||||
*/
|
||||
final class OAuth2AuthorizationRequestDeserializer extends JsonDeserializer<OAuth2AuthorizationRequest> {
|
||||
|
||||
private static final StdConverter<JsonNode, AuthorizationGrantType> AUTHORIZATION_GRANT_TYPE_CONVERTER = new StdConverters.AuthorizationGrantTypeConverter();
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizationRequest deserialize(JsonParser parser, DeserializationContext context)
|
||||
throws IOException {
|
||||
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
|
||||
JsonNode root = mapper.readTree(parser);
|
||||
return deserialize(parser, mapper, root);
|
||||
}
|
||||
|
||||
private OAuth2AuthorizationRequest deserialize(JsonParser parser, ObjectMapper mapper, JsonNode root)
|
||||
throws JsonParseException {
|
||||
AuthorizationGrantType authorizationGrantType = AUTHORIZATION_GRANT_TYPE_CONVERTER
|
||||
.convert(JsonNodeUtils.findObjectNode(root, "authorizationGrantType"));
|
||||
Builder builder = getBuilder(parser, authorizationGrantType);
|
||||
builder.authorizationUri(JsonNodeUtils.findStringValue(root, "authorizationUri"));
|
||||
builder.clientId(JsonNodeUtils.findStringValue(root, "clientId"));
|
||||
builder.redirectUri(JsonNodeUtils.findStringValue(root, "redirectUri"));
|
||||
builder.scopes(JsonNodeUtils.findValue(root, "scopes", JsonNodeUtils.STRING_SET, mapper));
|
||||
builder.state(JsonNodeUtils.findStringValue(root, "state"));
|
||||
builder.additionalParameters(
|
||||
JsonNodeUtils.findValue(root, "additionalParameters", JsonNodeUtils.STRING_OBJECT_MAP, mapper));
|
||||
builder.authorizationRequestUri(JsonNodeUtils.findStringValue(root, "authorizationRequestUri"));
|
||||
builder.attributes(JsonNodeUtils.findValue(root, "attributes", JsonNodeUtils.STRING_OBJECT_MAP, mapper));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private Builder getBuilder(JsonParser parser,
|
||||
AuthorizationGrantType authorizationGrantType) throws JsonParseException {
|
||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) {
|
||||
return OAuth2AuthorizationRequest.authorizationCode();
|
||||
}
|
||||
if (AuthorizationGrantType.IMPLICIT.equals(authorizationGrantType)) {
|
||||
return OAuth2AuthorizationRequest.implicit();
|
||||
}
|
||||
throw new JsonParseException(parser, "Invalid authorizationGrantType");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.authorization.jackson2;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is a straight copy from Spring Security.
|
||||
* It should be consolidated when merging this codebase into Spring Security.
|
||||
*
|
||||
* This mixin class is used to serialize/deserialize {@link OAuth2AuthorizationRequest}.
|
||||
* It also registers a custom deserializer {@link OAuth2AuthorizationRequestDeserializer}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.3
|
||||
* @see OAuth2AuthorizationRequest
|
||||
* @see OAuth2AuthorizationRequestDeserializer
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
@JsonDeserialize(using = OAuth2AuthorizationRequestDeserializer.class)
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
abstract class OAuth2AuthorizationRequestMixin {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.jackson2;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
|
||||
import com.fasterxml.jackson.core.Version;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
|
||||
/**
|
||||
* Jackson {@code Module} for {@code spring-authorization-server}, that registers the
|
||||
* following mix-in annotations:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link UnmodifiableMapMixin}</li>
|
||||
* <li>{@link HashSetMixin}</li>
|
||||
* <li>{@link OAuth2AuthorizationRequestMixin}</li>
|
||||
* <li>{@link DurationMixin}</li>
|
||||
* <li>{@link JwsAlgorithmMixin}</li>
|
||||
* <li>{@link OAuth2TokenFormatMixin}</li>
|
||||
* </ul>
|
||||
*
|
||||
* If not already enabled, default typing will be automatically enabled as type info is
|
||||
* required to properly serialize/deserialize objects. In order to use this module just
|
||||
* add it to your {@code ObjectMapper} configuration.
|
||||
*
|
||||
* <pre>
|
||||
* ObjectMapper mapper = new ObjectMapper();
|
||||
* mapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
|
||||
* </pre>
|
||||
*
|
||||
* <b>NOTE:</b> Use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get a list
|
||||
* of all security modules.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.1.2
|
||||
* @see SecurityJackson2Modules
|
||||
* @see UnmodifiableMapMixin
|
||||
* @see HashSetMixin
|
||||
* @see OAuth2AuthorizationRequestMixin
|
||||
* @see DurationMixin
|
||||
* @see JwsAlgorithmMixin
|
||||
* @see OAuth2TokenFormatMixin
|
||||
*/
|
||||
public class OAuth2AuthorizationServerJackson2Module extends SimpleModule {
|
||||
|
||||
public OAuth2AuthorizationServerJackson2Module() {
|
||||
super(OAuth2AuthorizationServerJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupModule(SetupContext context) {
|
||||
SecurityJackson2Modules.enableDefaultTyping(context.getOwner());
|
||||
context.setMixInAnnotations(Collections.unmodifiableMap(Collections.emptyMap()).getClass(),
|
||||
UnmodifiableMapMixin.class);
|
||||
context.setMixInAnnotations(HashSet.class, HashSetMixin.class);
|
||||
context.setMixInAnnotations(LinkedHashSet.class, HashSetMixin.class);
|
||||
context.setMixInAnnotations(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class);
|
||||
context.setMixInAnnotations(Duration.class, DurationMixin.class);
|
||||
context.setMixInAnnotations(SignatureAlgorithm.class, JwsAlgorithmMixin.class);
|
||||
context.setMixInAnnotations(MacAlgorithm.class, JwsAlgorithmMixin.class);
|
||||
context.setMixInAnnotations(OAuth2TokenFormat.class, OAuth2TokenFormatMixin.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.jackson2;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenFormat;
|
||||
|
||||
/**
|
||||
* This mixin class is used to serialize/deserialize {@link OAuth2TokenFormat}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.3
|
||||
* @see OAuth2TokenFormat
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
abstract class OAuth2TokenFormatMixin {
|
||||
|
||||
@JsonCreator
|
||||
OAuth2TokenFormatMixin(@JsonProperty("value") String value) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.authorization.jackson2;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.util.StdConverter;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is a straight copy from Spring Security.
|
||||
* It should be consolidated when merging this codebase into Spring Security.
|
||||
*
|
||||
* {@code StdConverter} implementations.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.3
|
||||
*/
|
||||
abstract class StdConverters {
|
||||
|
||||
static final class AccessTokenTypeConverter extends StdConverter<JsonNode, OAuth2AccessToken.TokenType> {
|
||||
|
||||
@Override
|
||||
public OAuth2AccessToken.TokenType convert(JsonNode jsonNode) {
|
||||
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
|
||||
if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(value)) {
|
||||
return OAuth2AccessToken.TokenType.BEARER;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static final class ClientAuthenticationMethodConverter extends StdConverter<JsonNode, ClientAuthenticationMethod> {
|
||||
|
||||
@Override
|
||||
public ClientAuthenticationMethod convert(JsonNode jsonNode) {
|
||||
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
|
||||
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equalsIgnoreCase(value)
|
||||
|| ClientAuthenticationMethod.BASIC.getValue().equalsIgnoreCase(value)) {
|
||||
return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
|
||||
}
|
||||
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equalsIgnoreCase(value)
|
||||
|| ClientAuthenticationMethod.POST.getValue().equalsIgnoreCase(value)) {
|
||||
return ClientAuthenticationMethod.CLIENT_SECRET_POST;
|
||||
}
|
||||
if (ClientAuthenticationMethod.NONE.getValue().equalsIgnoreCase(value)) {
|
||||
return ClientAuthenticationMethod.NONE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static final class AuthorizationGrantTypeConverter extends StdConverter<JsonNode, AuthorizationGrantType> {
|
||||
|
||||
@Override
|
||||
public AuthorizationGrantType convert(JsonNode jsonNode) {
|
||||
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
|
||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equalsIgnoreCase(value)) {
|
||||
return AuthorizationGrantType.AUTHORIZATION_CODE;
|
||||
}
|
||||
if (AuthorizationGrantType.IMPLICIT.getValue().equalsIgnoreCase(value)) {
|
||||
return AuthorizationGrantType.IMPLICIT;
|
||||
}
|
||||
if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equalsIgnoreCase(value)) {
|
||||
return AuthorizationGrantType.CLIENT_CREDENTIALS;
|
||||
}
|
||||
if (AuthorizationGrantType.PASSWORD.getValue().equalsIgnoreCase(value)) {
|
||||
return AuthorizationGrantType.PASSWORD;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static final class AuthenticationMethodConverter extends StdConverter<JsonNode, AuthenticationMethod> {
|
||||
|
||||
@Override
|
||||
public AuthenticationMethod convert(JsonNode jsonNode) {
|
||||
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
|
||||
if (AuthenticationMethod.HEADER.getValue().equalsIgnoreCase(value)) {
|
||||
return AuthenticationMethod.HEADER;
|
||||
}
|
||||
if (AuthenticationMethod.FORM.getValue().equalsIgnoreCase(value)) {
|
||||
return AuthenticationMethod.FORM;
|
||||
}
|
||||
if (AuthenticationMethod.QUERY.getValue().equalsIgnoreCase(value)) {
|
||||
return AuthenticationMethod.QUERY;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.oauth2.server.authorization.jackson2;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is a straight copy from Spring Security.
|
||||
* It should be consolidated when merging this codebase into Spring Security.
|
||||
*
|
||||
* A {@code JsonDeserializer} for {@link Collections#unmodifiableMap(Map)}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.3
|
||||
* @see Collections#unmodifiableMap(Map)
|
||||
* @see UnmodifiableMapMixin
|
||||
*/
|
||||
final class UnmodifiableMapDeserializer extends JsonDeserializer<Map<?, ?>> {
|
||||
|
||||
@Override
|
||||
public Map<?, ?> deserialize(JsonParser parser, DeserializationContext context) throws IOException {
|
||||
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
|
||||
JsonNode mapNode = mapper.readTree(parser);
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
if (mapNode != null && mapNode.isObject()) {
|
||||
Iterable<Map.Entry<String, JsonNode>> fields = mapNode::fields;
|
||||
for (Map.Entry<String, JsonNode> field : fields) {
|
||||
result.put(field.getKey(), mapper.readValue(field.getValue().traverse(mapper), Object.class));
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user