Compare commits
153 Commits
Author | SHA1 | Date | |
---|---|---|---|
Sambo Chea | 7d3acd887a | ||
Norbert Nowak | 658b186381 | ||
Joe Grandja | d33ec32017 | ||
Daniel Garnier-Moiroux | 59040a4c3d | ||
Joshua Casey | 3b0938883b | ||
Daniel Garnier-Moiroux | 1962b9c5b7 | ||
Sambo Chea | d11ac9a2ee | ||
Joe Grandja | a90d98aa1e | ||
Joe Grandja | e440935c14 | ||
Joe Grandja | 1f9e0f56fb | ||
Joe Grandja | 7ff237ed90 | ||
Joe Grandja | d6d74bc843 | ||
Joe Grandja | 277565599a | ||
Joe Grandja | 3b4cb382ec | ||
Joe Grandja | 16c6ed4fb4 | ||
Joe Grandja | 4cc1553542 | ||
Joe Grandja | 4b45d69c17 | ||
Joe Grandja | 4f3d7c4821 | ||
Joe Grandja | 799d790423 | ||
Joe Grandja | 9f1c760876 | ||
Joe Grandja | b25b906917 | ||
Joe Grandja | 839123409d | ||
Joe Grandja | 03d3879817 | ||
Joe Grandja | ef314e5c28 | ||
Joe Grandja | a7d4d45658 | ||
Joe Grandja | 69a34bce5b | ||
Joe Grandja | 7652d0ebbe | ||
Joe Grandja | b5d47366ad | ||
Joe Grandja | ece5f2b3b1 | ||
Joe Grandja | c00226d0c6 | ||
Joe Grandja | 6ffda38cb9 | ||
Joe Grandja | 09846eebeb | ||
Joe Grandja | c9afc3e061 | ||
Joe Grandja | afd5491ced | ||
Joe Grandja | 313b4cc5d3 | ||
Joe Grandja | 3c6571044d | ||
Joe Grandja | 2f1684d44b | ||
Joe Grandja | 2cdb7ef0fc | ||
Joe Grandja | ee1b46b9a6 | ||
Joe Grandja | cee5aacc15 | ||
Joe Grandja | fd9df9e2e7 | ||
Joe Grandja | 7261b40cd5 | ||
Joe Grandja | 41541912e6 | ||
Joe Grandja | bffcbc5440 | ||
Joe Grandja | 218d49b134 | ||
Joe Grandja | 1fa0161164 | ||
Joe Grandja | adf96b4e25 | ||
Joe Grandja | 3f310eec00 | ||
Florian Berthe | aeab08579a | ||
Joe Grandja | 8e5e5873f5 | ||
Joe Grandja | 39ed820560 | ||
Joe Grandja | 5b8d0c3301 | ||
Joe Grandja | 698d45cdbd | ||
Joe Grandja | dc2fe30570 | ||
Gerardo Roza | 4bcc1afac7 | ||
Joe Grandja | 17c20e98d4 | ||
Joe Grandja | b7996e26d0 | ||
Joe Grandja | 12f4001c9d | ||
Joe Grandja | 4b37606807 | ||
Joe Grandja | 36e66bd732 | ||
Joe Grandja | 42a89d15b1 | ||
Joe Grandja | 259b55f682 | ||
Joe Grandja | b6932ed25e | ||
Joe Grandja | f9f15227d8 | ||
Joe Grandja | 668bb069f2 | ||
Joe Grandja | f2bb523105 | ||
Joe Grandja | 8c71e56350 | ||
Laurentiu Spilca | 7c7e664bb7 | ||
Laurentiu Spilca | 7fae37f0b5 | ||
Joe Grandja | 7f8aff7982 | ||
Joe Grandja | f077337e43 | ||
Joe Grandja | 79f1cf5a50 | ||
Joe Grandja | f97b8b2656 | ||
Joe Grandja | 4e4656f7bb | ||
Joe Grandja | eb97e12f56 | ||
Joe Grandja | ab591dc39d | ||
Daniel Garnier-Moiroux | 6a5e277a11 | ||
Joe Grandja | 43fbd9d345 | ||
Joe Grandja | d97235d0bb | ||
Joe Grandja | 9f246fc304 | ||
Joe Grandja | 6db194da27 | ||
Joe Grandja | f0ecb5b93f | ||
Joe Grandja | c1e9c1d76c | ||
Joe Grandja | a9423c6b13 | ||
Joe Grandja | 8100568613 | ||
Anoop Garlapati | e1f491bd61 | ||
Joe Grandja | 90fbbea126 | ||
Joe Grandja | d6fc405bb1 | ||
Joe Grandja | db4dd3f08e | ||
Joe Grandja | 6e2f2fe8a4 | ||
Joe Grandja | 061d9f8c18 | ||
Joe Grandja | 59f77d034e | ||
Joe Grandja | bf24cfb19e | ||
Joe Grandja | 77a9b2ebf3 | ||
Joe Grandja | d76d209124 | ||
Joe Grandja | 58ad2d2c6c | ||
Joe Grandja | 1a6b3e3e59 | ||
Joe Grandja | e61639bf7f | ||
Joe Grandja | 7e2264204b | ||
Joe Grandja | 43b44a1f77 | ||
Joe Grandja | 19d6e97372 | ||
Joe Grandja | cff7b786de | ||
Joe Grandja | edf23562cb | ||
Joe Grandja | e7909d0cdd | ||
Joe Grandja | e49d4a79b4 | ||
Joe Grandja | 7720e275e4 | ||
Joe Grandja | 6a2c841d06 | ||
Joe Grandja | d7fe79d0ec | ||
Joe Grandja | 40ca7a4654 | ||
Joe Grandja | 06bf391bfa | ||
Joe Grandja | bfb5432b46 | ||
Joe Grandja | 9818618ea3 | ||
Joe Grandja | cb09aef605 | ||
Joe Grandja | ebcdf7989d | ||
Joe Grandja | df8793c902 | ||
Joe Grandja | 6c7486429c | ||
Joe Grandja | cf82c06502 | ||
Joe Grandja | a2167a5091 | ||
Alexey Nesterov | 78d4bd0bad | ||
Alexey Nesterov | 1ce77d3caa | ||
Joe Grandja | b7ddb837d6 | ||
Vivek Babu | dc94e5e161 | ||
Joe Grandja | 18f8b3afaa | ||
Joe Grandja | 601640e4fa | ||
Joe Grandja | af60f3d4d0 | ||
Joe Grandja | 7b1b965c08 | ||
Joe Grandja | 0c6b1251ce | ||
Joe Grandja | ad00d78e1a | ||
Joe Grandja | 5471c94615 | ||
Walid EL ALAOUY | 84cdf82c0d | ||
Joe Grandja | 45283b42b4 | ||
Joe Grandja | 142876068d | ||
Joe Grandja | ca94d02abc | ||
Joe Grandja | 628c8bece3 | ||
Joe Grandja | f0013fc062 | ||
Joe Grandja | aa5133e170 | ||
Joe Grandja | 5c31fb1b7e | ||
Joe Grandja | e5fdee3034 | ||
Daniel Garnier-Moiroux | ab090445b3 | ||
Joe Grandja | 8541f6be69 | ||
Joe Grandja | ae20f73676 | ||
Joe Grandja | 4091d69d0c | ||
Joe Grandja | c3b254579c | ||
Joe Grandja | 22bf1eb951 | ||
Joe Grandja | 35ecdae190 | ||
Joe Grandja | b12ffe2ae4 | ||
Joe Grandja | 117b312ac4 | ||
Joe Grandja | f7c84957bb | ||
Joe Grandja | 5a030568ce | ||
Joe Grandja | 72ec2633f8 | ||
Joe Grandja | 0b95672c47 | ||
Joe Grandja | d51f12f5d2 | ||
Joe Grandja | f3a27101be |
|
@ -3,6 +3,3 @@ contact_links:
|
|||
- name: Community Support
|
||||
url: https://stackoverflow.com/questions/tagged/spring-security
|
||||
about: Please ask and answer questions on StackOverflow with the tag `spring-security`.
|
||||
- name: Security Issues
|
||||
url: https://pivotal.io/security#reporting
|
||||
about: Please report security vulnerabilities here.
|
||||
|
|
|
@ -32,13 +32,14 @@ try {
|
|||
checkout scm
|
||||
sh "git clean -dfx"
|
||||
try {
|
||||
withCredentials([GRADLE_ENTERPRISE_CACHE_USER,
|
||||
withCredentials([ARTIFACTORY_CREDENTIALS,
|
||||
GRADLE_ENTERPRISE_CACHE_USER,
|
||||
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY]) {
|
||||
withEnv([jdkEnv(),
|
||||
"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 --stacktrace"
|
||||
sh "./gradlew check -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --stacktrace"
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
|
|
|
@ -17,6 +17,8 @@ This project uses https://www.zenhub.com/[ZenHub] to prioritize the feature road
|
|||
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].
|
||||
|
||||
== 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].
|
||||
|
@ -43,7 +45,7 @@ This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code
|
|||
By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||
|
||||
== Downloading Artifacts
|
||||
See https://github.com/spring-projects/spring-framework/wiki/Downloading-Spring-artifacts[downloading Spring artifacts] for Maven repository information.
|
||||
See https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Artifacts[downloading Spring artifacts] for Maven repository information.
|
||||
|
||||
== Building from Source
|
||||
Spring Authorization Server uses a https://gradle.org[Gradle]-based build system.
|
||||
|
|
14
build.gradle
14
build.gradle
|
@ -1,11 +1,19 @@
|
|||
buildscript {
|
||||
dependencies {
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.33.RELEASE'
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.37'
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.5.RELEASE'
|
||||
}
|
||||
repositories {
|
||||
maven { url 'https://repo.spring.io/plugins-snapshot' }
|
||||
maven {
|
||||
url = 'https://repo.spring.io/plugins-snapshot'
|
||||
if (project.hasProperty('artifactoryUsername')) {
|
||||
credentials {
|
||||
username "$artifactoryUsername"
|
||||
password "$artifactoryPassword"
|
||||
}
|
||||
}
|
||||
}
|
||||
maven { url 'https://plugins.gradle.org/m2/' }
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +25,8 @@ apply plugin: 'io.spring.convention.root'
|
|||
group = 'org.springframework.security.experimental'
|
||||
description = 'Spring Authorization Server'
|
||||
|
||||
ext.snapshotBuild = version.contains("SNAPSHOT")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
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 ? 'master' : 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" }
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
@import 'spring.css';
|
||||
|
||||
a code {
|
||||
color: #097dff;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
= 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.0.1
|
||||
springBootVersion=2.4.0-M2
|
||||
version=0.1.1-SNAPSHOT
|
||||
springBootVersion=2.4.3
|
||||
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.2.+"
|
||||
ext.springVersion = "5.3.3"
|
||||
}
|
||||
|
||||
if (!project.hasProperty("springSecurityVersion")) {
|
||||
ext.springSecurityVersion = "5.3.+"
|
||||
ext.springSecurityVersion = "5.4.5"
|
||||
}
|
||||
|
||||
if (!project.hasProperty("reactorVersion")) {
|
||||
ext.reactorVersion = "Dysprosium-SR+"
|
||||
ext.reactorVersion = "2020.0.3"
|
||||
}
|
||||
|
||||
if (!project.hasProperty("locksDisabled")) {
|
||||
|
@ -21,47 +21,16 @@ 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"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
dependency "com.nimbusds:oauth2-oidc-sdk:latest.release"
|
||||
dependency "com.nimbusds:nimbus-jose-jwt:latest.release"
|
||||
dependency "com.fasterxml.jackson.core:jackson-databind:2.+"
|
||||
dependency "javax.servlet:javax.servlet-api:4.+"
|
||||
dependency 'junit:junit:latest.release'
|
||||
dependency 'org.assertj:assertj-core:latest.release'
|
||||
dependency 'org.mockito:mockito-core:latest.release'
|
||||
dependency "com.squareup.okhttp3:mockwebserver:3.+"
|
||||
dependency "com.squareup.okhttp3:okhttp:3.+"
|
||||
dependency "com.jayway.jsonpath:json-path:2.+"
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
NOTE:
|
||||
The latest `reactor-netty` dependency was split into `reactor-netty-core` and `reactor-netty-http`,
|
||||
which resulted in the snapshot build to fail. The below configuration fixes it.
|
||||
|
||||
Reference:
|
||||
- https://github.com/spring-projects/spring-security/issues/8909
|
||||
- https://github.com/reactor/reactor-netty/issues/739#issuecomment-667047117
|
||||
*/
|
||||
if (reactorVersion.startsWith('20')) {
|
||||
if (reactorVersion.endsWith('SNAPSHOT') || reactorVersion.endsWith('+')) {
|
||||
ext.reactorLatestVersion = "latest.integration"
|
||||
} else {
|
||||
ext.reactorLatestVersion = "latest.release"
|
||||
}
|
||||
configurations {
|
||||
all {
|
||||
resolutionStrategy {
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.name == 'reactor-netty') {
|
||||
details.useTarget("${details.requested.group}:reactor-netty-http:${reactorLatestVersion}")
|
||||
details.because("reactor-netty is now split into reactor-netty-core and reactor-netty-http")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 "com.squareup.okhttp3:mockwebserver:3.14.9"
|
||||
dependency "com.squareup.okhttp3:okhttp:3.14.9"
|
||||
dependency "com.jayway.jsonpath:json-path:2.4.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
|
@ -1,3 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
|
@ -1,23 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
antlr:antlr:2.7.7
|
||||
ch.qos.logback:logback-classic:1.2.3
|
||||
ch.qos.logback:logback-core:1.2.3
|
||||
com.google.code.findbugs:jsr305:3.0.2
|
||||
com.google.errorprone:error_prone_annotations:2.2.0
|
||||
com.google.guava:failureaccess:1.0.1
|
||||
com.google.guava:guava:27.1-jre
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
|
||||
com.google.j2objc:j2objc-annotations:1.1
|
||||
com.puppycrawl.tools:checkstyle:8.22
|
||||
commons-beanutils:commons-beanutils:1.9.3
|
||||
commons-collections:commons-collections:3.2.2
|
||||
info.picocli:picocli:3.9.6
|
||||
io.spring.javaformat:spring-javaformat-checkstyle:0.0.15
|
||||
io.spring.nohttp:nohttp-checkstyle:0.0.3.RELEASE
|
||||
io.spring.nohttp:nohttp:0.0.3.RELEASE
|
||||
net.sf.saxon:Saxon-HE:9.9.1-3
|
||||
org.antlr:antlr4-runtime:4.7.2
|
||||
org.checkerframework:checker-qual:2.5.2
|
||||
org.codehaus.mojo:animal-sniffer-annotations:1.17
|
|
@ -1,23 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-core:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-databind:2.11.2
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1
|
||||
com.nimbusds:nimbus-jose-jwt:8.20
|
||||
net.minidev:accessors-smart:1.2
|
||||
net.minidev:json-smart:2.3
|
||||
org.ow2.asm:asm:5.0.4
|
||||
org.springframework.security:spring-security-config:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-jose:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-web:5.3.4.RELEASE
|
||||
org.springframework:spring-aop:5.2.8.RELEASE
|
||||
org.springframework:spring-beans:5.2.8.RELEASE
|
||||
org.springframework:spring-context:5.2.8.RELEASE
|
||||
org.springframework:spring-core:5.2.8.RELEASE
|
||||
org.springframework:spring-expression:5.2.8.RELEASE
|
||||
org.springframework:spring-jcl:5.2.8.RELEASE
|
||||
org.springframework:spring-web:5.2.8.RELEASE
|
|
@ -1,23 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-core:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-databind:2.11.2
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1
|
||||
com.nimbusds:nimbus-jose-jwt:8.20
|
||||
net.minidev:accessors-smart:1.2
|
||||
net.minidev:json-smart:2.3
|
||||
org.ow2.asm:asm:5.0.4
|
||||
org.springframework.security:spring-security-config:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-jose:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-web:5.3.4.RELEASE
|
||||
org.springframework:spring-aop:5.2.8.RELEASE
|
||||
org.springframework:spring-beans:5.2.8.RELEASE
|
||||
org.springframework:spring-context:5.2.8.RELEASE
|
||||
org.springframework:spring-core:5.2.8.RELEASE
|
||||
org.springframework:spring-expression:5.2.8.RELEASE
|
||||
org.springframework:spring-jcl:5.2.8.RELEASE
|
||||
org.springframework:spring-web:5.2.8.RELEASE
|
|
@ -1,3 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
|
@ -1,23 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-core:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-databind:2.11.2
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1
|
||||
com.nimbusds:nimbus-jose-jwt:8.20
|
||||
net.minidev:accessors-smart:1.2
|
||||
net.minidev:json-smart:2.3
|
||||
org.ow2.asm:asm:5.0.4
|
||||
org.springframework.security:spring-security-config:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-jose:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-web:5.3.4.RELEASE
|
||||
org.springframework:spring-aop:5.2.8.RELEASE
|
||||
org.springframework:spring-beans:5.2.8.RELEASE
|
||||
org.springframework:spring-context:5.2.8.RELEASE
|
||||
org.springframework:spring-core:5.2.8.RELEASE
|
||||
org.springframework:spring-expression:5.2.8.RELEASE
|
||||
org.springframework:spring-jcl:5.2.8.RELEASE
|
||||
org.springframework:spring-web:5.2.8.RELEASE
|
|
@ -1,4 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
org.jacoco:org.jacoco.agent:0.8.5
|
|
@ -1,11 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
org.jacoco:org.jacoco.agent:0.8.5
|
||||
org.jacoco:org.jacoco.ant:0.8.5
|
||||
org.jacoco:org.jacoco.core:0.8.5
|
||||
org.jacoco:org.jacoco.report:0.8.5
|
||||
org.ow2.asm:asm-analysis:7.2
|
||||
org.ow2.asm:asm-commons:7.2
|
||||
org.ow2.asm:asm-tree:7.2
|
||||
org.ow2.asm:asm:7.2
|
|
@ -1,3 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
|
@ -1,3 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
|
@ -1,4 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
javax.servlet:javax.servlet-api:4.0.1
|
|
@ -1,23 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-core:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-databind:2.11.2
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1
|
||||
com.nimbusds:nimbus-jose-jwt:8.20
|
||||
net.minidev:accessors-smart:1.2
|
||||
net.minidev:json-smart:2.3
|
||||
org.ow2.asm:asm:5.0.4
|
||||
org.springframework.security:spring-security-config:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-jose:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-web:5.3.4.RELEASE
|
||||
org.springframework:spring-aop:5.2.8.RELEASE
|
||||
org.springframework:spring-beans:5.2.8.RELEASE
|
||||
org.springframework:spring-context:5.2.8.RELEASE
|
||||
org.springframework:spring-core:5.2.8.RELEASE
|
||||
org.springframework:spring-expression:5.2.8.RELEASE
|
||||
org.springframework:spring-jcl:5.2.8.RELEASE
|
||||
org.springframework:spring-web:5.2.8.RELEASE
|
|
@ -1,23 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-core:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-databind:2.11.2
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1
|
||||
com.nimbusds:nimbus-jose-jwt:8.20
|
||||
net.minidev:accessors-smart:1.2
|
||||
net.minidev:json-smart:2.3
|
||||
org.ow2.asm:asm:5.0.4
|
||||
org.springframework.security:spring-security-config:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-jose:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-web:5.3.4.RELEASE
|
||||
org.springframework:spring-aop:5.2.8.RELEASE
|
||||
org.springframework:spring-beans:5.2.8.RELEASE
|
||||
org.springframework:spring-context:5.2.8.RELEASE
|
||||
org.springframework:spring-core:5.2.8.RELEASE
|
||||
org.springframework:spring-expression:5.2.8.RELEASE
|
||||
org.springframework:spring-jcl:5.2.8.RELEASE
|
||||
org.springframework:spring-web:5.2.8.RELEASE
|
|
@ -1,3 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
|
@ -1,3 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
|
@ -1,3 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
|
@ -1,35 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-core:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-databind:2.11.2
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1
|
||||
com.jayway.jsonpath:json-path:2.4.0
|
||||
com.nimbusds:nimbus-jose-jwt:8.20
|
||||
junit:junit:4.13
|
||||
net.bytebuddy:byte-buddy-agent:1.10.13
|
||||
net.bytebuddy:byte-buddy:1.10.13
|
||||
net.minidev:accessors-smart:1.2
|
||||
net.minidev:json-smart:2.3
|
||||
org.assertj:assertj-core:3.16.1
|
||||
org.hamcrest:hamcrest-core:1.3
|
||||
org.mockito:mockito-core:3.5.2
|
||||
org.objenesis:objenesis:3.1
|
||||
org.ow2.asm:asm:5.0.4
|
||||
org.slf4j:slf4j-api:1.7.25
|
||||
org.springframework.security:spring-security-config:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-jose:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-test:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-web:5.3.4.RELEASE
|
||||
org.springframework:spring-aop:5.2.8.RELEASE
|
||||
org.springframework:spring-beans:5.2.8.RELEASE
|
||||
org.springframework:spring-context:5.2.8.RELEASE
|
||||
org.springframework:spring-core:5.2.8.RELEASE
|
||||
org.springframework:spring-expression:5.2.8.RELEASE
|
||||
org.springframework:spring-jcl:5.2.8.RELEASE
|
||||
org.springframework:spring-test:5.2.8.RELEASE
|
||||
org.springframework:spring-web:5.2.8.RELEASE
|
||||
org.springframework:spring-webmvc:5.2.8.RELEASE
|
|
@ -1,35 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-core:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-databind:2.11.2
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1
|
||||
com.jayway.jsonpath:json-path:2.4.0
|
||||
com.nimbusds:nimbus-jose-jwt:8.20
|
||||
junit:junit:4.13
|
||||
net.bytebuddy:byte-buddy-agent:1.10.13
|
||||
net.bytebuddy:byte-buddy:1.10.13
|
||||
net.minidev:accessors-smart:1.2
|
||||
net.minidev:json-smart:2.3
|
||||
org.assertj:assertj-core:3.16.1
|
||||
org.hamcrest:hamcrest-core:1.3
|
||||
org.mockito:mockito-core:3.5.2
|
||||
org.objenesis:objenesis:3.1
|
||||
org.ow2.asm:asm:5.0.4
|
||||
org.slf4j:slf4j-api:1.7.25
|
||||
org.springframework.security:spring-security-config:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-jose:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-test:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-web:5.3.4.RELEASE
|
||||
org.springframework:spring-aop:5.2.8.RELEASE
|
||||
org.springframework:spring-beans:5.2.8.RELEASE
|
||||
org.springframework:spring-context:5.2.8.RELEASE
|
||||
org.springframework:spring-core:5.2.8.RELEASE
|
||||
org.springframework:spring-expression:5.2.8.RELEASE
|
||||
org.springframework:spring-jcl:5.2.8.RELEASE
|
||||
org.springframework:spring-test:5.2.8.RELEASE
|
||||
org.springframework:spring-web:5.2.8.RELEASE
|
||||
org.springframework:spring-webmvc:5.2.8.RELEASE
|
|
@ -1,3 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
|
@ -1,35 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-core:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-databind:2.11.2
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1
|
||||
com.jayway.jsonpath:json-path:2.4.0
|
||||
com.nimbusds:nimbus-jose-jwt:8.20
|
||||
junit:junit:4.13
|
||||
net.bytebuddy:byte-buddy-agent:1.10.13
|
||||
net.bytebuddy:byte-buddy:1.10.13
|
||||
net.minidev:accessors-smart:1.2
|
||||
net.minidev:json-smart:2.3
|
||||
org.assertj:assertj-core:3.16.1
|
||||
org.hamcrest:hamcrest-core:1.3
|
||||
org.mockito:mockito-core:3.5.2
|
||||
org.objenesis:objenesis:3.1
|
||||
org.ow2.asm:asm:5.0.4
|
||||
org.slf4j:slf4j-api:1.7.25
|
||||
org.springframework.security:spring-security-config:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-jose:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-test:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-web:5.3.4.RELEASE
|
||||
org.springframework:spring-aop:5.2.8.RELEASE
|
||||
org.springframework:spring-beans:5.2.8.RELEASE
|
||||
org.springframework:spring-context:5.2.8.RELEASE
|
||||
org.springframework:spring-core:5.2.8.RELEASE
|
||||
org.springframework:spring-expression:5.2.8.RELEASE
|
||||
org.springframework:spring-jcl:5.2.8.RELEASE
|
||||
org.springframework:spring-test:5.2.8.RELEASE
|
||||
org.springframework:spring-web:5.2.8.RELEASE
|
||||
org.springframework:spring-webmvc:5.2.8.RELEASE
|
|
@ -1,35 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-core:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-databind:2.11.2
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1
|
||||
com.jayway.jsonpath:json-path:2.4.0
|
||||
com.nimbusds:nimbus-jose-jwt:8.20
|
||||
junit:junit:4.13
|
||||
net.bytebuddy:byte-buddy-agent:1.10.13
|
||||
net.bytebuddy:byte-buddy:1.10.13
|
||||
net.minidev:accessors-smart:1.2
|
||||
net.minidev:json-smart:2.3
|
||||
org.assertj:assertj-core:3.16.1
|
||||
org.hamcrest:hamcrest-core:1.3
|
||||
org.mockito:mockito-core:3.5.2
|
||||
org.objenesis:objenesis:3.1
|
||||
org.ow2.asm:asm:5.0.4
|
||||
org.slf4j:slf4j-api:1.7.25
|
||||
org.springframework.security:spring-security-config:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-jose:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-test:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-web:5.3.4.RELEASE
|
||||
org.springframework:spring-aop:5.2.8.RELEASE
|
||||
org.springframework:spring-beans:5.2.8.RELEASE
|
||||
org.springframework:spring-context:5.2.8.RELEASE
|
||||
org.springframework:spring-core:5.2.8.RELEASE
|
||||
org.springframework:spring-expression:5.2.8.RELEASE
|
||||
org.springframework:spring-jcl:5.2.8.RELEASE
|
||||
org.springframework:spring-test:5.2.8.RELEASE
|
||||
org.springframework:spring-web:5.2.8.RELEASE
|
||||
org.springframework:spring-webmvc:5.2.8.RELEASE
|
|
@ -1,35 +0,0 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-core:2.11.2
|
||||
com.fasterxml.jackson.core:jackson-databind:2.11.2
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1
|
||||
com.jayway.jsonpath:json-path:2.4.0
|
||||
com.nimbusds:nimbus-jose-jwt:8.20
|
||||
junit:junit:4.13
|
||||
net.bytebuddy:byte-buddy-agent:1.10.13
|
||||
net.bytebuddy:byte-buddy:1.10.13
|
||||
net.minidev:accessors-smart:1.2
|
||||
net.minidev:json-smart:2.3
|
||||
org.assertj:assertj-core:3.16.1
|
||||
org.hamcrest:hamcrest-core:1.3
|
||||
org.mockito:mockito-core:3.5.2
|
||||
org.objenesis:objenesis:3.1
|
||||
org.ow2.asm:asm:5.0.4
|
||||
org.slf4j:slf4j-api:1.7.25
|
||||
org.springframework.security:spring-security-config:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-core:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-oauth2-jose:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-test:5.3.4.RELEASE
|
||||
org.springframework.security:spring-security-web:5.3.4.RELEASE
|
||||
org.springframework:spring-aop:5.2.8.RELEASE
|
||||
org.springframework:spring-beans:5.2.8.RELEASE
|
||||
org.springframework:spring-context:5.2.8.RELEASE
|
||||
org.springframework:spring-core:5.2.8.RELEASE
|
||||
org.springframework:spring-expression:5.2.8.RELEASE
|
||||
org.springframework:spring-jcl:5.2.8.RELEASE
|
||||
org.springframework:spring-test:5.2.8.RELEASE
|
||||
org.springframework:spring-web:5.2.8.RELEASE
|
||||
org.springframework:spring-webmvc:5.2.8.RELEASE
|
|
@ -20,5 +20,5 @@ dependencies {
|
|||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion = '0.8.5'
|
||||
toolVersion = '0.8.6'
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -17,21 +17,44 @@ package org.springframework.security.config.annotation.web.configuration;
|
|||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||
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.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
/**
|
||||
* {@link Configuration} for OAuth 2.0 Authorization Server support.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see OAuth2AuthorizationServerConfigurer
|
||||
*/
|
||||
@Configuration
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class OAuth2AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
public WebSecurityConfigurer<WebSecurity> defaultOAuth2AuthorizationServerSecurity() {
|
||||
return new OAuth2AuthorizationServerSecurity();
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
applyDefaultSecurity(http);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
public static void applyDefaultSecurity(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer<>();
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer
|
||||
.getEndpointsMatcher();
|
||||
|
||||
http
|
||||
.requestMatcher(endpointsMatcher)
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
|
||||
.apply(authorizationServerConfigurer);
|
||||
}
|
||||
// @formatter:on
|
||||
}
|
||||
|
|
|
@ -1,59 +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.config.annotation.web.configuration;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
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.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
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 static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
/**
|
||||
* {@link WebSecurityConfigurerAdapter} providing default security configuration for OAuth 2.0 Authorization Server.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public class OAuth2AuthorizationServerSecurity extends WebSecurityConfigurerAdapter {
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer<>();
|
||||
|
||||
http
|
||||
.requestMatcher(new OrRequestMatcher(authorizationServerConfigurer.getEndpointMatchers()))
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(withDefaults())
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(tokenEndpointMatcher()))
|
||||
.apply(authorizationServerConfigurer);
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
private static RequestMatcher tokenEndpointMatcher() {
|
||||
return new AntPathRequestMatcher(
|
||||
OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI,
|
||||
HttpMethod.POST.name());
|
||||
}
|
||||
}
|
|
@ -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,60 +15,86 @@
|
|||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.LinkedHashMap;
|
||||
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.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
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.keys.KeyManager;
|
||||
import org.springframework.security.oauth2.jose.jws.NimbusJwsEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
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.OAuth2TokenRevocationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.web.JwkSetEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
|
||||
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.OAuth2ClientAuthenticationFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.0.1
|
||||
* @see AbstractHttpConfigurer
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2AuthorizationEndpointFilter
|
||||
* @see OAuth2TokenEndpointFilter
|
||||
* @see OAuth2TokenRevocationEndpointFilter
|
||||
* @see NimbusJwkSetEndpointFilter
|
||||
* @see OidcProviderConfigurationEndpointFilter
|
||||
* @see OAuth2ClientAuthenticationFilter
|
||||
*/
|
||||
public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer<B>, B> {
|
||||
|
||||
private final RequestMatcher authorizationEndpointMatcher = new AntPathRequestMatcher(
|
||||
OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI, HttpMethod.GET.name());
|
||||
private final RequestMatcher tokenEndpointMatcher = new AntPathRequestMatcher(
|
||||
OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI, HttpMethod.POST.name());
|
||||
private final RequestMatcher jwkSetEndpointMatcher = new AntPathRequestMatcher(
|
||||
JwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI, HttpMethod.GET.name());
|
||||
private RequestMatcher authorizationEndpointMatcher;
|
||||
private RequestMatcher tokenEndpointMatcher;
|
||||
private RequestMatcher tokenRevocationEndpointMatcher;
|
||||
private RequestMatcher jwkSetEndpointMatcher;
|
||||
private RequestMatcher oidcProviderConfigurationEndpointMatcher;
|
||||
private final RequestMatcher endpointsMatcher = (request) ->
|
||||
this.authorizationEndpointMatcher.matches(request) ||
|
||||
this.tokenEndpointMatcher.matches(request) ||
|
||||
this.tokenRevocationEndpointMatcher.matches(request) ||
|
||||
this.jwkSetEndpointMatcher.matches(request) ||
|
||||
this.oidcProviderConfigurationEndpointMatcher.matches(request);
|
||||
|
||||
/**
|
||||
* Sets the repository of registered clients.
|
||||
|
@ -95,98 +121,179 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the key manager.
|
||||
* Sets the provider settings.
|
||||
*
|
||||
* @param keyManager the key manager
|
||||
* @param providerSettings the provider settings
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer<B> keyManager(KeyManager keyManager) {
|
||||
Assert.notNull(keyManager, "keyManager cannot be null");
|
||||
this.getBuilder().setSharedObject(KeyManager.class, keyManager);
|
||||
public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings providerSettings) {
|
||||
Assert.notNull(providerSettings, "providerSettings cannot be null");
|
||||
this.getBuilder().setSharedObject(ProviderSettings.class, providerSettings);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link RequestMatcher}'s for the authorization server endpoints.
|
||||
* Returns a {@link RequestMatcher} for the authorization server endpoints.
|
||||
*
|
||||
* @return a {@code List} of {@link RequestMatcher}'s for the authorization server endpoints
|
||||
* @return a {@link RequestMatcher} for the authorization server endpoints
|
||||
*/
|
||||
public List<RequestMatcher> getEndpointMatchers() {
|
||||
return Arrays.asList(this.authorizationEndpointMatcher,
|
||||
this.tokenEndpointMatcher, this.jwkSetEndpointMatcher);
|
||||
public RequestMatcher getEndpointsMatcher() {
|
||||
return this.endpointsMatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(B builder) {
|
||||
ProviderSettings providerSettings = getProviderSettings(builder);
|
||||
validateProviderSettings(providerSettings);
|
||||
initEndpointMatchers(providerSettings);
|
||||
|
||||
OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
|
||||
new OAuth2ClientAuthenticationProvider(
|
||||
getRegisteredClientRepository(builder));
|
||||
getRegisteredClientRepository(builder),
|
||||
getAuthorizationService(builder));
|
||||
builder.authenticationProvider(postProcess(clientAuthenticationProvider));
|
||||
|
||||
NimbusJwsEncoder jwtEncoder = new NimbusJwsEncoder(getKeyManager(builder));
|
||||
JwtEncoder jwtEncoder = getJwtEncoder(builder);
|
||||
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(builder);
|
||||
|
||||
OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
|
||||
new OAuth2AuthorizationCodeAuthenticationProvider(
|
||||
getRegisteredClientRepository(builder),
|
||||
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));
|
||||
|
||||
OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
|
||||
new OAuth2TokenRevocationAuthenticationProvider(
|
||||
getAuthorizationService(builder));
|
||||
builder.authenticationProvider(postProcess(tokenRevocationAuthenticationProvider));
|
||||
|
||||
ExceptionHandlingConfigurer<B> exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptionHandling != null) {
|
||||
// Register the default AuthenticationEntryPoint for the token endpoint
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(
|
||||
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), this.tokenEndpointMatcher);
|
||||
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
|
||||
entryPoints.put(
|
||||
new OrRequestMatcher(
|
||||
this.tokenEndpointMatcher,
|
||||
this.tokenRevocationEndpointMatcher),
|
||||
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
|
||||
DelegatingAuthenticationEntryPoint authenticationEntryPoint =
|
||||
new DelegatingAuthenticationEntryPoint(entryPoints);
|
||||
|
||||
// TODO This needs to change as the login page could be customized with a different URL
|
||||
authenticationEntryPoint.setDefaultEntryPoint(
|
||||
new LoginUrlAuthenticationEntryPoint(
|
||||
DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL));
|
||||
|
||||
exceptionHandling.authenticationEntryPoint(authenticationEntryPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(B builder) {
|
||||
JwkSetEndpointFilter jwkSetEndpointFilter = new JwkSetEndpointFilter(getKeyManager(builder));
|
||||
ProviderSettings providerSettings = getProviderSettings(builder);
|
||||
if (providerSettings.issuer() != null) {
|
||||
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
|
||||
new OidcProviderConfigurationEndpointFilter(providerSettings);
|
||||
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
}
|
||||
|
||||
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
|
||||
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
|
||||
jwkSource,
|
||||
providerSettings.jwkSetEndpoint());
|
||||
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
|
||||
OAuth2ClientAuthenticationFilter clientAuthenticationFilter = new OAuth2ClientAuthenticationFilter(
|
||||
authenticationManager, this.tokenEndpointMatcher);
|
||||
OAuth2ClientAuthenticationFilter clientAuthenticationFilter =
|
||||
new OAuth2ClientAuthenticationFilter(
|
||||
authenticationManager,
|
||||
new OrRequestMatcher(
|
||||
this.tokenEndpointMatcher,
|
||||
this.tokenRevocationEndpointMatcher));
|
||||
builder.addFilterAfter(postProcess(clientAuthenticationFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
|
||||
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
|
||||
new OAuth2AuthorizationEndpointFilter(
|
||||
getRegisteredClientRepository(builder),
|
||||
getAuthorizationService(builder));
|
||||
getAuthorizationService(builder),
|
||||
providerSettings.authorizationEndpoint());
|
||||
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
|
||||
OAuth2TokenEndpointFilter tokenEndpointFilter =
|
||||
new OAuth2TokenEndpointFilter(
|
||||
authenticationManager,
|
||||
getAuthorizationService(builder));
|
||||
providerSettings.tokenEndpoint());
|
||||
builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
|
||||
|
||||
OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
|
||||
new OAuth2TokenRevocationEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.tokenRevocationEndpoint());
|
||||
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenEndpointFilter.class);
|
||||
}
|
||||
|
||||
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.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());
|
||||
}
|
||||
|
||||
private static void validateProviderSettings(ProviderSettings providerSettings) {
|
||||
if (providerSettings.issuer() != null) {
|
||||
try {
|
||||
new URI(providerSettings.issuer()).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 = getRegisteredClientRepositoryBean(builder);
|
||||
registeredClientRepository = getBean(builder, RegisteredClientRepository.class);
|
||||
builder.setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
|
||||
}
|
||||
return registeredClientRepository;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> RegisteredClientRepository getRegisteredClientRepositoryBean(B builder) {
|
||||
return builder.getSharedObject(ApplicationContext.class).getBean(RegisteredClientRepository.class);
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService getAuthorizationService(B builder) {
|
||||
OAuth2AuthorizationService authorizationService = builder.getSharedObject(OAuth2AuthorizationService.class);
|
||||
if (authorizationService == null) {
|
||||
authorizationService = getAuthorizationServiceBean(builder);
|
||||
authorizationService = getOptionalBean(builder, OAuth2AuthorizationService.class);
|
||||
if (authorizationService == null) {
|
||||
authorizationService = new InMemoryOAuth2AuthorizationService();
|
||||
}
|
||||
|
@ -195,27 +302,90 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
|||
return authorizationService;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService getAuthorizationServiceBean(B builder) {
|
||||
Map<String, OAuth2AuthorizationService> authorizationServiceMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
|
||||
builder.getSharedObject(ApplicationContext.class), OAuth2AuthorizationService.class);
|
||||
if (authorizationServiceMap.size() > 1) {
|
||||
throw new NoUniqueBeanDefinitionException(OAuth2AuthorizationService.class, authorizationServiceMap.size(),
|
||||
"Expected single matching bean of type '" + OAuth2AuthorizationService.class.getName() + "' but found " +
|
||||
authorizationServiceMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(authorizationServiceMap.keySet()));
|
||||
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 (!authorizationServiceMap.isEmpty() ? authorizationServiceMap.values().iterator().next() : null);
|
||||
return jwtEncoder;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> KeyManager getKeyManager(B builder) {
|
||||
KeyManager keyManager = builder.getSharedObject(KeyManager.class);
|
||||
if (keyManager == null) {
|
||||
keyManager = getKeyManagerBean(builder);
|
||||
builder.setSharedObject(KeyManager.class, keyManager);
|
||||
@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 keyManager;
|
||||
return jwkSource;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> KeyManager getKeyManagerBean(B builder) {
|
||||
return builder.getSharedObject(ApplicationContext.class).getBean(KeyManager.class);
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,58 +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.crypto.keys;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for the management of {@link ManagedKey}(s),
|
||||
* e.g. {@code javax.crypto.SecretKey}, {@code java.security.PrivateKey}, {@code java.security.PublicKey}, etc.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see ManagedKey
|
||||
*/
|
||||
public interface KeyManager {
|
||||
|
||||
/**
|
||||
* Returns the {@link ManagedKey} identified by the provided {@code keyId},
|
||||
* or {@code null} if not found.
|
||||
*
|
||||
* @param keyId the key ID
|
||||
* @return the {@link ManagedKey}, or {@code null} if not found
|
||||
*/
|
||||
@Nullable
|
||||
ManagedKey findByKeyId(String keyId);
|
||||
|
||||
/**
|
||||
* Returns a {@code Set} of {@link ManagedKey}(s) having the provided key {@code algorithm},
|
||||
* or an empty {@code Set} if not found.
|
||||
*
|
||||
* @param algorithm the key algorithm
|
||||
* @return a {@code Set} of {@link ManagedKey}(s), or an empty {@code Set} if not found
|
||||
*/
|
||||
Set<ManagedKey> findByAlgorithm(String algorithm);
|
||||
|
||||
/**
|
||||
* Returns a {@code Set} of the {@link ManagedKey}(s).
|
||||
*
|
||||
* @return a {@code Set} of the {@link ManagedKey}(s)
|
||||
*/
|
||||
Set<ManagedKey> getKeys();
|
||||
|
||||
}
|
|
@ -1,246 +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.crypto.keys;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion2;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.Serializable;
|
||||
import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A {@code java.security.Key} that is managed by a {@link KeyManager}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see KeyManager
|
||||
*/
|
||||
public final class ManagedKey implements Serializable {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
private Key key;
|
||||
private PublicKey publicKey;
|
||||
private String keyId;
|
||||
private Instant activatedOn;
|
||||
private Instant deactivatedOn;
|
||||
|
||||
private ManagedKey() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this is a symmetric key, {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if this is a symmetric key, {@code false} otherwise
|
||||
*/
|
||||
public boolean isSymmetric() {
|
||||
return SecretKey.class.isAssignableFrom(this.key.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this is a asymmetric key, {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if this is a asymmetric key, {@code false} otherwise
|
||||
*/
|
||||
public boolean isAsymmetric() {
|
||||
return PrivateKey.class.isAssignableFrom(this.key.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a type of {@code java.security.Key},
|
||||
* e.g. {@code javax.crypto.SecretKey} or {@code java.security.PrivateKey}.
|
||||
*
|
||||
* @param <T> the type of {@code java.security.Key}
|
||||
* @return the type of {@code java.security.Key}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Key> T getKey() {
|
||||
return (T) this.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code java.security.PublicKey} if this is a asymmetric key, {@code null} otherwise.
|
||||
*
|
||||
* @return the {@code java.security.PublicKey} if this is a asymmetric key, {@code null} otherwise
|
||||
*/
|
||||
@Nullable
|
||||
public PublicKey getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key ID.
|
||||
*
|
||||
* @return the key ID
|
||||
*/
|
||||
public String getKeyId() {
|
||||
return this.keyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time when this key was activated.
|
||||
*
|
||||
* @return the time when this key was activated
|
||||
*/
|
||||
public Instant getActivatedOn() {
|
||||
return this.activatedOn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time when this key was deactivated, {@code null} if still active.
|
||||
*
|
||||
* @return the time when this key was deactivated, {@code null} if still active
|
||||
*/
|
||||
@Nullable
|
||||
public Instant getDeactivatedOn() {
|
||||
return this.deactivatedOn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this key is active, {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if this key is active, {@code false} otherwise
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return getDeactivatedOn() == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key algorithm.
|
||||
*
|
||||
* @return the key algorithm
|
||||
*/
|
||||
public String getAlgorithm() {
|
||||
return this.key.getAlgorithm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ManagedKey that = (ManagedKey) obj;
|
||||
return Objects.equals(this.keyId, that.keyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.keyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@code javax.crypto.SecretKey}.
|
||||
*
|
||||
* @param secretKey the {@code javax.crypto.SecretKey}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withSymmetricKey(SecretKey secretKey) {
|
||||
return new Builder(secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided
|
||||
* {@code java.security.PublicKey} and {@code java.security.PrivateKey}.
|
||||
*
|
||||
* @param publicKey the {@code java.security.PublicKey}
|
||||
* @param privateKey the {@code java.security.PrivateKey}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withAsymmetricKey(PublicKey publicKey, PrivateKey privateKey) {
|
||||
return new Builder(publicKey, privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link ManagedKey}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private Key key;
|
||||
private PublicKey publicKey;
|
||||
private String keyId;
|
||||
private Instant activatedOn;
|
||||
private Instant deactivatedOn;
|
||||
|
||||
private Builder(SecretKey secretKey) {
|
||||
Assert.notNull(secretKey, "secretKey cannot be null");
|
||||
this.key = secretKey;
|
||||
}
|
||||
|
||||
private Builder(PublicKey publicKey, PrivateKey privateKey) {
|
||||
Assert.notNull(publicKey, "publicKey cannot be null");
|
||||
Assert.notNull(privateKey, "privateKey cannot be null");
|
||||
this.key = privateKey;
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the key ID.
|
||||
*
|
||||
* @param keyId the key ID
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder keyId(String keyId) {
|
||||
this.keyId = keyId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time when this key was activated.
|
||||
*
|
||||
* @param activatedOn the time when this key was activated
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder activatedOn(Instant activatedOn) {
|
||||
this.activatedOn = activatedOn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time when this key was deactivated.
|
||||
*
|
||||
* @param deactivatedOn the time when this key was deactivated
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder deactivatedOn(Instant deactivatedOn) {
|
||||
this.deactivatedOn = deactivatedOn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link ManagedKey}.
|
||||
*
|
||||
* @return a {@link ManagedKey}
|
||||
*/
|
||||
public ManagedKey build() {
|
||||
Assert.hasText(this.keyId, "keyId cannot be empty");
|
||||
Assert.notNull(this.activatedOn, "activatedOn cannot be null");
|
||||
|
||||
ManagedKey managedKey = new ManagedKey();
|
||||
managedKey.key = this.key;
|
||||
managedKey.publicKey = this.publicKey;
|
||||
managedKey.keyId = this.keyId;
|
||||
managedKey.activatedOn = this.activatedOn;
|
||||
managedKey.deactivatedOn = this.deactivatedOn;
|
||||
return managedKey;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,89 +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.crypto.keys;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.security.KeyPair;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateRsaKey;
|
||||
import static org.springframework.security.crypto.keys.KeyGeneratorUtils.generateSecretKey;
|
||||
|
||||
/**
|
||||
* An implementation of a {@link KeyManager} that generates the {@link ManagedKey}(s) when constructed.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see KeyManager
|
||||
*/
|
||||
public final class StaticKeyGeneratingKeyManager implements KeyManager {
|
||||
private final Map<String, ManagedKey> keys;
|
||||
|
||||
public StaticKeyGeneratingKeyManager() {
|
||||
this.keys = Collections.unmodifiableMap(new HashMap<>(generateKeys()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ManagedKey findByKeyId(String keyId) {
|
||||
Assert.hasText(keyId, "keyId cannot be empty");
|
||||
return this.keys.get(keyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ManagedKey> findByAlgorithm(String algorithm) {
|
||||
Assert.hasText(algorithm, "algorithm cannot be empty");
|
||||
return this.keys.values().stream()
|
||||
.filter(managedKey -> managedKey.getAlgorithm().equals(algorithm))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ManagedKey> getKeys() {
|
||||
return new HashSet<>(this.keys.values());
|
||||
}
|
||||
|
||||
private static Map<String, ManagedKey> generateKeys() {
|
||||
KeyPair rsaKeyPair = generateRsaKey();
|
||||
ManagedKey rsaManagedKey = ManagedKey.withAsymmetricKey(rsaKeyPair.getPublic(), rsaKeyPair.getPrivate())
|
||||
.keyId(UUID.randomUUID().toString())
|
||||
.activatedOn(Instant.now())
|
||||
.build();
|
||||
|
||||
SecretKey hmacKey = generateSecretKey();
|
||||
ManagedKey secretManagedKey = ManagedKey.withSymmetricKey(hmacKey)
|
||||
.keyId(UUID.randomUUID().toString())
|
||||
.activatedOn(Instant.now())
|
||||
.build();
|
||||
|
||||
return Stream.of(rsaManagedKey, secretManagedKey)
|
||||
.collect(Collectors.toMap(ManagedKey::getKeyId, v -> v));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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/issues/9184">Issue gh-9184</a>
|
||||
*/
|
||||
public interface OAuth2ErrorCodes2 extends OAuth2ErrorCodes {
|
||||
|
||||
String UNSUPPORTED_TOKEN_TYPE = "unsupported_token_type";
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -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,27 +13,40 @@
|
|||
* 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.core.SpringSecurityCoreVersion2;
|
||||
import org.springframework.util.Assert;
|
||||
package org.springframework.security.oauth2.core;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Standard token types defined in the OAuth Token Type Hints Registry.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-4.1.2">4.1.2 OAuth Token Type Hints Registry</a>
|
||||
*/
|
||||
public final class TokenType implements Serializable {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
public static final TokenType ACCESS_TOKEN = new TokenType("access_token");
|
||||
public static final TokenType AUTHORIZATION_CODE = new TokenType("authorization_code");
|
||||
public final class OAuth2TokenType implements Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
public static final OAuth2TokenType ACCESS_TOKEN = new OAuth2TokenType("access_token");
|
||||
public static final OAuth2TokenType REFRESH_TOKEN = new OAuth2TokenType("refresh_token");
|
||||
private final String value;
|
||||
|
||||
public TokenType(String value) {
|
||||
/**
|
||||
* Constructs an {@code OAuth2TokenType} using the provided value.
|
||||
*
|
||||
* @param value the value of the token type
|
||||
*/
|
||||
public OAuth2TokenType(String value) {
|
||||
Assert.hasText(value, "value cannot be empty");
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the token type.
|
||||
*
|
||||
* @return the value of the token type
|
||||
*/
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
@ -46,12 +59,12 @@ public final class TokenType implements Serializable {
|
|||
if (obj == null || this.getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
TokenType that = (TokenType) obj;
|
||||
return this.getValue().equals(that.getValue());
|
||||
OAuth2TokenType that = (OAuth2TokenType) obj;
|
||||
return getValue().equals(that.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.getValue().hashCode();
|
||||
return getValue().hashCode();
|
||||
}
|
||||
}
|
|
@ -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,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.core;
|
||||
package org.springframework.security.oauth2.core;
|
||||
|
||||
/**
|
||||
* Internal class used for serialization across Spring Security Authorization Server classes.
|
||||
|
@ -21,9 +21,9 @@ package org.springframework.security.core;
|
|||
* @author Anoop Garlapati
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public final class SpringSecurityCoreVersion2 {
|
||||
public final class Version {
|
||||
private static final int MAJOR = 0;
|
||||
private static final int MINOR = 0;
|
||||
private static final int MINOR = 1;
|
||||
private static final int PATCH = 1;
|
||||
|
||||
/**
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.context;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A facility for holding information associated to a specific context.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
*/
|
||||
public interface Context {
|
||||
|
||||
@Nullable
|
||||
<V> V get(Object key);
|
||||
|
||||
@Nullable
|
||||
default <V> V get(Class<V> key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
V value = get((Object) key);
|
||||
return key.isInstance(value) ? value : null;
|
||||
}
|
||||
|
||||
boolean hasKey(Object key);
|
||||
|
||||
static Context of(Map<Object, Object> context) {
|
||||
return new DefaultContext(context);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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.context;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class DefaultContext implements Context {
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
DefaultContext(Map<Object, Object> context) {
|
||||
Assert.notNull(context, "context cannot be null");
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
@Nullable
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.endpoint;
|
||||
|
||||
/**
|
||||
* 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/issues/9183">Issue gh-9183</a>
|
||||
*/
|
||||
public interface OAuth2ParameterNames2 extends OAuth2ParameterNames {
|
||||
|
||||
String TOKEN = "token";
|
||||
|
||||
String TOKEN_TYPE_HINT = "token_type_hint";
|
||||
|
||||
}
|
|
@ -0,0 +1,358 @@
|
|||
/*
|
||||
* 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.oidc;
|
||||
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A representation of an OpenID Provider Configuration Response,
|
||||
* which is returned from an Issuer's Discovery Endpoint,
|
||||
* and contains a set of claims about the OpenID Provider's configuration.
|
||||
* The claims are defined by the OpenID Connect Discovery 1.0 specification.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.1.0
|
||||
* @see OidcProviderMetadataClaimAccessor
|
||||
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
|
||||
*/
|
||||
public final class OidcProviderConfiguration implements OidcProviderMetadataClaimAccessor, Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
private OidcProviderConfiguration(Map<String, Object> claims) {
|
||||
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OpenID Provider Configuration metadata.
|
||||
*
|
||||
* @return a {@code Map} of the metadata values
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> getClaims() {
|
||||
return this.claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with empty claims.
|
||||
*
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided claims.
|
||||
*
|
||||
* @param claims the claims to initialize the builder
|
||||
*/
|
||||
public static Builder withClaims(Map<String, Object> claims) {
|
||||
Assert.notEmpty(claims, "claims cannot be empty");
|
||||
return new Builder()
|
||||
.claims(c -> c.putAll(claims));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helps configure an {@link OidcProviderConfiguration}
|
||||
*/
|
||||
public static class Builder {
|
||||
private final Map<String, Object> claims = new LinkedHashMap<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@code issuer} in the resulting {@link OidcProviderConfiguration}, REQUIRED.
|
||||
*
|
||||
* @param issuer the URL of the OpenID Provider's Issuer Identifier
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder issuer(String issuer) {
|
||||
return claim(OidcProviderMetadataClaimNames.ISSUER, issuer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@code authorization_endpoint} in the resulting {@link OidcProviderConfiguration}, REQUIRED.
|
||||
*
|
||||
* @param authorizationEndpoint the URL of the OpenID Provider's OAuth 2.0 Authorization Endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder authorizationEndpoint(String authorizationEndpoint) {
|
||||
return claim(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@code token_endpoint} in the resulting {@link OidcProviderConfiguration}, REQUIRED.
|
||||
*
|
||||
* @param tokenEndpoint the URL of the OpenID Provider's OAuth 2.0 Token Endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder tokenEndpoint(String tokenEndpoint) {
|
||||
return claim(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, tokenEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this Authentication Method to the collection of {@code token_endpoint_auth_methods_supported}
|
||||
* in the resulting {@link OidcProviderConfiguration}, OPTIONAL.
|
||||
*
|
||||
* @param authenticationMethod the OAuth 2.0 Authentication Method supported by the Token endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder tokenEndpointAuthenticationMethod(String authenticationMethod) {
|
||||
addClaimToClaimList(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the Token Endpoint Authentication Method(s) allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param authenticationMethodsConsumer a {@code Consumer} of the Token Endpoint Authentication Method(s)
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder tokenEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
|
||||
acceptClaimValues(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@code jwks_uri} in the resulting {@link OidcProviderConfiguration}, REQUIRED.
|
||||
*
|
||||
* @param jwkSetUri the URL of the OpenID Provider's JSON Web Key Set document
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder jwkSetUri(String jwkSetUri) {
|
||||
return claim(OidcProviderMetadataClaimNames.JWKS_URI, jwkSetUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this Response Type to the collection of {@code response_types_supported} in the resulting
|
||||
* {@link OidcProviderConfiguration}, REQUIRED.
|
||||
*
|
||||
* @param responseType the OAuth 2.0 {@code response_type} value that the OpenID Provider supports
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder responseType(String responseType) {
|
||||
addClaimToClaimList(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseType);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the Response Type(s) allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param responseTypesConsumer a {@code Consumer} of the Response Type(s)
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder responseTypes(Consumer<List<String>> responseTypesConsumer) {
|
||||
acceptClaimValues(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseTypesConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this Grant Type to the collection of {@code grant_types_supported} in the resulting
|
||||
* {@link OidcProviderConfiguration}, OPTIONAL.
|
||||
*
|
||||
* @param grantType the OAuth 2.0 {@code grant_type} value that the OpenID Provider supports
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder grantType(String grantType) {
|
||||
addClaimToClaimList(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantType);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the Grant Type(s) allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param grantTypesConsumer a {@code Consumer} of the Grant Type(s)
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder grantTypes(Consumer<List<String>> grantTypesConsumer) {
|
||||
acceptClaimValues(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantTypesConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this Subject Type to the collection of {@code subject_types_supported} in the resulting
|
||||
* {@link OidcProviderConfiguration}, REQUIRED.
|
||||
*
|
||||
* @param subjectType the Subject Type that the OpenID Provider supports
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder subjectType(String subjectType) {
|
||||
addClaimToClaimList(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, subjectType);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the Subject Types(s) allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param subjectTypesConsumer a {@code Consumer} of the Subject Types(s)
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder subjectTypes(Consumer<List<String>> subjectTypesConsumer) {
|
||||
acceptClaimValues(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, subjectTypesConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this Scope to the collection of {@code scopes_supported} in the resulting
|
||||
* {@link OidcProviderConfiguration}, RECOMMENDED.
|
||||
*
|
||||
* @param scope the OAuth 2.0 {@code scope} value that the OpenID Provider supports
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder scope(String scope) {
|
||||
addClaimToClaimList(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, scope);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the Scopes(s) allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param scopesConsumer a {@code Consumer} of the Scopes(s)
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder scopes(Consumer<List<String>> scopesConsumer) {
|
||||
acceptClaimValues(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, scopesConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this {@link JwsAlgorithm JWS} signing algorithm to the collection of {@code id_token_signing_alg_values_supported}
|
||||
* in the resulting {@link OidcProviderConfiguration}, REQUIRED.
|
||||
*
|
||||
* @param signingAlgorithm the {@link JwsAlgorithm JWS} signing algorithm supported for the {@link OidcIdToken ID Token}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder idTokenSigningAlgorithm(String signingAlgorithm) {
|
||||
addClaimToClaimList(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, signingAlgorithm);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} of the {@link JwsAlgorithm JWS} signing algorithms for the {@link OidcIdToken ID Token}
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param signingAlgorithmsConsumer a {@code Consumer} of the {@link JwsAlgorithm JWS} signing algorithms for the {@link OidcIdToken ID Token}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder idTokenSigningAlgorithms(Consumer<List<String>> signingAlgorithmsConsumer) {
|
||||
acceptClaimValues(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, signingAlgorithmsConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this claim in the resulting {@link OidcProviderConfiguration}.
|
||||
*
|
||||
* @param name the claim name
|
||||
* @param value the claim value
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder claim(String name, Object value) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
this.claims.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to every {@link #claim(String, Object)} declared so far with
|
||||
* the possibility to add, replace, or remove.
|
||||
*
|
||||
* @param claimsConsumer a {@code Consumer} of the claims
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
|
||||
claimsConsumer.accept(this.claims);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the claims and build the {@link OidcProviderConfiguration}.
|
||||
* <p>
|
||||
* The following claims are REQUIRED:
|
||||
* {@code issuer}, {@code authorization_endpoint}, {@code token_endpoint}, {@code jwks_uri},
|
||||
* {@code response_types_supported} and {@code subject_types_supported}.
|
||||
*
|
||||
* @return the {@link OidcProviderConfiguration}
|
||||
*/
|
||||
public OidcProviderConfiguration build() {
|
||||
validateClaims();
|
||||
return new OidcProviderConfiguration(this.claims);
|
||||
}
|
||||
|
||||
private void validateClaims() {
|
||||
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.ISSUER), "issuer cannot be null");
|
||||
validateURL(this.claims.get(OidcProviderMetadataClaimNames.ISSUER), "issuer must be a valid URL");
|
||||
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint cannot be null");
|
||||
validateURL(this.claims.get(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL");
|
||||
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint cannot be null");
|
||||
validateURL(this.claims.get(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint must be a valid URL");
|
||||
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.JWKS_URI), "jwksUri cannot be null");
|
||||
validateURL(this.claims.get(OidcProviderMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL");
|
||||
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be null");
|
||||
Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes must be of type List");
|
||||
Assert.notEmpty((List<?>) this.claims.get(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be empty");
|
||||
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be null");
|
||||
Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes must be of type List");
|
||||
Assert.notEmpty((List<?>) this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be empty");
|
||||
Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be null");
|
||||
Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms must be of type List");
|
||||
Assert.notEmpty((List<?>) this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be empty");
|
||||
}
|
||||
|
||||
private static void validateURL(Object url, String errorMessage) {
|
||||
if (URL.class.isAssignableFrom(url.getClass())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new URI(url.toString()).toURL();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void addClaimToClaimList(String name, String value) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
|
||||
((List<String>) this.claims.get(name)).add(value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
|
||||
this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
|
||||
List<String> values = (List<String>) this.claims.get(name);
|
||||
valuesConsumer.accept(values);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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.oidc;
|
||||
|
||||
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link ClaimAccessor} for the "claims" that can be returned
|
||||
* in the OpenID Provider Configuration Response.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.1.0
|
||||
* @see ClaimAccessor
|
||||
* @see OidcProviderMetadataClaimNames
|
||||
* @see OidcProviderConfiguration
|
||||
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
|
||||
*/
|
||||
public interface OidcProviderMetadataClaimAccessor extends ClaimAccessor {
|
||||
|
||||
/**
|
||||
* Returns the {@code URL} the OpenID Provider asserts as its Issuer Identifier {@code (issuer)}.
|
||||
*
|
||||
* @return the {@code URL} the OpenID Provider asserts as its Issuer Identifier
|
||||
*/
|
||||
default URL getIssuer() {
|
||||
return getClaimAsURL(OidcProviderMetadataClaimNames.ISSUER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code URL} of the OAuth 2.0 Authorization Endpoint {@code (authorization_endpoint)}.
|
||||
*
|
||||
* @return the {@code URL} of the OAuth 2.0 Authorization Endpoint
|
||||
*/
|
||||
default URL getAuthorizationEndpoint() {
|
||||
return getClaimAsURL(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code URL} of the OAuth 2.0 Token Endpoint {@code (token_endpoint)}.
|
||||
*
|
||||
* @return the {@code URL} of the OAuth 2.0 Token Endpoint
|
||||
*/
|
||||
default URL getTokenEndpoint() {
|
||||
return getClaimAsURL(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the client authentication methods supported by the OAuth 2.0 Token Endpoint {@code (token_endpoint_auth_methods_supported)}.
|
||||
*
|
||||
* @return the client authentication methods supported by the OAuth 2.0 Token Endpoint
|
||||
*/
|
||||
default List<String> getTokenEndpointAuthenticationMethods() {
|
||||
return getClaimAsStringList(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code URL} of the JSON Web Key Set {@code (jwks_uri)}.
|
||||
*
|
||||
* @return the {@code URL} of the JSON Web Key Set
|
||||
*/
|
||||
default URL getJwkSetUri() {
|
||||
return getClaimAsURL(OidcProviderMetadataClaimNames.JWKS_URI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OAuth 2.0 {@code response_type} values supported {@code (response_types_supported)}.
|
||||
*
|
||||
* @return the OAuth 2.0 {@code response_type} values supported
|
||||
*/
|
||||
default List<String> getResponseTypes() {
|
||||
return getClaimAsStringList(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OAuth 2.0 {@code grant_type} values supported {@code (grant_types_supported)}.
|
||||
*
|
||||
* @return the OAuth 2.0 {@code grant_type} values supported
|
||||
*/
|
||||
default List<String> getGrantTypes() {
|
||||
return getClaimAsStringList(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Subject Identifier types supported {@code (subject_types_supported)}.
|
||||
*
|
||||
* @return the Subject Identifier types supported
|
||||
*/
|
||||
default List<String> getSubjectTypes() {
|
||||
return getClaimAsStringList(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OAuth 2.0 {@code scope} values supported {@code (scopes_supported)}.
|
||||
*
|
||||
* @return the OAuth 2.0 {@code scope} values supported
|
||||
*/
|
||||
default List<String> getScopes() {
|
||||
return getClaimAsStringList(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JwsAlgorithm JWS} signing algorithms supported for the {@link OidcIdToken ID Token}
|
||||
* to encode the claims in a {@link Jwt} {@code (id_token_signing_alg_values_supported)}.
|
||||
*
|
||||
* @return the {@link JwsAlgorithm JWS} signing algorithms supported for the {@link OidcIdToken ID Token}
|
||||
*/
|
||||
default List<String> getIdTokenSigningAlgorithms() {
|
||||
return getClaimAsStringList(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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.oidc;
|
||||
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
|
||||
/**
|
||||
* The names of the "claims" defined by OpenID Connect Discovery 1.0 that can be returned
|
||||
* in the OpenID Provider Configuration Response.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.1.0
|
||||
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
|
||||
*/
|
||||
public interface OidcProviderMetadataClaimNames {
|
||||
|
||||
/**
|
||||
* {@code issuer} - the {@code URL} the OpenID Provider asserts as its Issuer Identifier
|
||||
*/
|
||||
String ISSUER = "issuer";
|
||||
|
||||
/**
|
||||
* {@code authorization_endpoint} - the {@code URL} of the OAuth 2.0 Authorization Endpoint
|
||||
*/
|
||||
String AUTHORIZATION_ENDPOINT = "authorization_endpoint";
|
||||
|
||||
/**
|
||||
* {@code token_endpoint} - the {@code URL} of the OAuth 2.0 Token Endpoint
|
||||
*/
|
||||
String TOKEN_ENDPOINT = "token_endpoint";
|
||||
|
||||
/**
|
||||
* {@code token_endpoint_auth_methods_supported} - the client authentication methods supported by the OAuth 2.0 Token Endpoint
|
||||
*/
|
||||
String TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED = "token_endpoint_auth_methods_supported";
|
||||
|
||||
/**
|
||||
* {@code jwks_uri} - the {@code URL} of the JSON Web Key Set
|
||||
*/
|
||||
String JWKS_URI = "jwks_uri";
|
||||
|
||||
/**
|
||||
* {@code response_types_supported} - the OAuth 2.0 {@code response_type} values supported
|
||||
*/
|
||||
String RESPONSE_TYPES_SUPPORTED = "response_types_supported";
|
||||
|
||||
/**
|
||||
* {@code grant_types_supported} - the OAuth 2.0 {@code grant_type} values supported
|
||||
*/
|
||||
String GRANT_TYPES_SUPPORTED = "grant_types_supported";
|
||||
|
||||
/**
|
||||
* {@code subject_types_supported} - the Subject Identifier types supported
|
||||
*/
|
||||
String SUBJECT_TYPES_SUPPORTED = "subject_types_supported";
|
||||
|
||||
/**
|
||||
* {@code scopes_supported} - the OAuth 2.0 {@code scope} values supported
|
||||
*/
|
||||
String SCOPES_SUPPORTED = "scopes_supported";
|
||||
|
||||
/**
|
||||
* {@code id_token_signing_alg_values_supported} - the {@link JwsAlgorithm JWS} signing algorithms supported for the {@link OidcIdToken ID Token}
|
||||
*/
|
||||
String ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = "id_token_signing_alg_values_supported";
|
||||
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author 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 org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.GsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* This class is a straight copy from Spring Security.
|
||||
* It should be consolidated when merging this codebase into Spring Security.
|
||||
*
|
||||
* Utility methods for {@link HttpMessageConverter}'s.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.1
|
||||
*/
|
||||
final class HttpMessageConverters {
|
||||
|
||||
private static final boolean jackson2Present;
|
||||
|
||||
private static final boolean gsonPresent;
|
||||
|
||||
private static final boolean jsonbPresent;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = HttpMessageConverters.class.getClassLoader();
|
||||
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
|
||||
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
|
||||
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
|
||||
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
|
||||
}
|
||||
|
||||
private HttpMessageConverters() {
|
||||
}
|
||||
|
||||
static GenericHttpMessageConverter<Object> getJsonMessageConverter() {
|
||||
if (jackson2Present) {
|
||||
return new MappingJackson2HttpMessageConverter();
|
||||
}
|
||||
if (gsonPresent) {
|
||||
return new GsonHttpMessageConverter();
|
||||
}
|
||||
if (jsonbPresent) {
|
||||
return new JsonbHttpMessageConverter();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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.oidc.http.converter;
|
||||
|
||||
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.OidcProviderConfiguration;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcProviderMetadataClaimNames;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link HttpMessageConverter} for an {@link OidcProviderConfiguration OpenID Provider Configuration Response}.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.1.0
|
||||
* @see AbstractHttpMessageConverter
|
||||
* @see OidcProviderConfiguration
|
||||
*/
|
||||
public class OidcProviderConfigurationHttpMessageConverter
|
||||
extends AbstractHttpMessageConverter<OidcProviderConfiguration> {
|
||||
|
||||
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>, OidcProviderConfiguration> providerConfigurationConverter = new OidcProviderConfigurationConverter();
|
||||
private Converter<OidcProviderConfiguration, Map<String, Object>> providerConfigurationParametersConverter = OidcProviderConfiguration::getClaims;
|
||||
|
||||
public OidcProviderConfigurationHttpMessageConverter() {
|
||||
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return OidcProviderConfiguration.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected OidcProviderConfiguration readInternal(Class<? extends OidcProviderConfiguration> clazz, HttpInputMessage inputMessage)
|
||||
throws HttpMessageNotReadableException {
|
||||
try {
|
||||
Map<String, Object> providerConfigurationParameters =
|
||||
(Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
|
||||
return this.providerConfigurationConverter.convert(providerConfigurationParameters);
|
||||
} catch (Exception ex) {
|
||||
throw new HttpMessageNotReadableException(
|
||||
"An error occurred reading the OpenID Provider Configuration: " + ex.getMessage(), ex, inputMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeInternal(OidcProviderConfiguration providerConfiguration, HttpOutputMessage outputMessage)
|
||||
throws HttpMessageNotWritableException {
|
||||
try {
|
||||
Map<String, Object> providerConfigurationResponseParameters =
|
||||
this.providerConfigurationParametersConverter.convert(providerConfiguration);
|
||||
this.jsonMessageConverter.write(
|
||||
providerConfigurationResponseParameters,
|
||||
STRING_OBJECT_MAP.getType(),
|
||||
MediaType.APPLICATION_JSON,
|
||||
outputMessage
|
||||
);
|
||||
} catch (Exception ex) {
|
||||
throw new HttpMessageNotWritableException(
|
||||
"An error occurred writing the OpenID Provider Configuration: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Converter} used for converting the OpenID Provider Configuration parameters
|
||||
* to an {@link OidcProviderConfiguration}.
|
||||
*
|
||||
* @param providerConfigurationConverter the {@link Converter} used for converting to an
|
||||
* {@link OidcProviderConfiguration}
|
||||
*/
|
||||
public final void setProviderConfigurationConverter(Converter<Map<String, Object>, OidcProviderConfiguration> providerConfigurationConverter) {
|
||||
Assert.notNull(providerConfigurationConverter, "providerConfigurationConverter cannot be null");
|
||||
this.providerConfigurationConverter = providerConfigurationConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Converter} used for converting the {@link OidcProviderConfiguration} to a
|
||||
* {@code Map} representation of the OpenID Provider Configuration.
|
||||
*
|
||||
* @param providerConfigurationParametersConverter the {@link Converter} used for converting to a
|
||||
* {@code Map} representation of the OpenID Provider Configuration
|
||||
*/
|
||||
public final void setProviderConfigurationParametersConverter(
|
||||
Converter<OidcProviderConfiguration, Map<String, Object>> providerConfigurationParametersConverter) {
|
||||
Assert.notNull(providerConfigurationParametersConverter, "providerConfigurationParametersConverter cannot be null");
|
||||
this.providerConfigurationParametersConverter = providerConfigurationParametersConverter;
|
||||
}
|
||||
|
||||
private static final class OidcProviderConfigurationConverter implements Converter<Map<String, Object>, OidcProviderConfiguration> {
|
||||
private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance();
|
||||
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 URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
|
||||
private final ClaimTypeConverter claimTypeConverter;
|
||||
|
||||
private OidcProviderConfigurationConverter() {
|
||||
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(OidcProviderMetadataClaimNames.ISSUER, urlConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, urlConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.JWKS_URI, urlConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, collectionStringConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, collectionStringConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, collectionStringConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, collectionStringConverter);
|
||||
claimConverters.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, collectionStringConverter);
|
||||
this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OidcProviderConfiguration convert(Map<String, Object> source) {
|
||||
Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
|
||||
return OidcProviderConfiguration.withClaims(parsedClaims).build();
|
||||
}
|
||||
|
||||
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
|
||||
return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,30 +13,19 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose;
|
||||
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.util.Assert;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.ALG;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.CRIT;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.CTY;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.JKU;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.JWK;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.KID;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.TYP;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5C;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5T;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5T_S256;
|
||||
import static org.springframework.security.oauth2.jose.JoseHeaderNames.X5U;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The JOSE header is a JSON object representing the header parameters of a JSON Web Token,
|
||||
|
@ -55,16 +44,16 @@ public final class JoseHeader {
|
|||
private final Map<String, Object> headers;
|
||||
|
||||
private JoseHeader(Map<String, Object> headers) {
|
||||
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
|
||||
this.headers = Collections.unmodifiableMap(new HashMap<>(headers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JWS algorithm used to digitally sign the JWS.
|
||||
* Returns the {@link JwsAlgorithm JWS algorithm} used to digitally sign the JWS.
|
||||
*
|
||||
* @return the JWS algorithm
|
||||
*/
|
||||
public JwsAlgorithm getJwsAlgorithm() {
|
||||
return getHeader(ALG);
|
||||
return getHeader(JoseHeaderNames.ALG);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,8 +62,8 @@ public final class JoseHeader {
|
|||
*
|
||||
* @return the JWK Set URL
|
||||
*/
|
||||
public String getJwkSetUri() {
|
||||
return getHeader(JKU);
|
||||
public URL getJwkSetUri() {
|
||||
return getHeader(JoseHeaderNames.JKU);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,7 +73,7 @@ public final class JoseHeader {
|
|||
* @return the JSON Web Key
|
||||
*/
|
||||
public Map<String, Object> getJwk() {
|
||||
return getHeader(JWK);
|
||||
return getHeader(JoseHeaderNames.JWK);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -93,7 +82,7 @@ public final class JoseHeader {
|
|||
* @return the key ID
|
||||
*/
|
||||
public String getKeyId() {
|
||||
return getHeader(KID);
|
||||
return getHeader(JoseHeaderNames.KID);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,8 +91,8 @@ public final class JoseHeader {
|
|||
*
|
||||
* @return the X.509 URL
|
||||
*/
|
||||
public String getX509Uri() {
|
||||
return getHeader(X5U);
|
||||
public URL getX509Uri() {
|
||||
return getHeader(JoseHeaderNames.X5U);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -113,7 +102,7 @@ public final class JoseHeader {
|
|||
* @return the X.509 certificate chain
|
||||
*/
|
||||
public List<String> getX509CertificateChain() {
|
||||
return getHeader(X5C);
|
||||
return getHeader(JoseHeaderNames.X5C);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -123,7 +112,7 @@ public final class JoseHeader {
|
|||
* @return the X.509 certificate SHA-1 thumbprint
|
||||
*/
|
||||
public String getX509SHA1Thumbprint() {
|
||||
return getHeader(X5T);
|
||||
return getHeader(JoseHeaderNames.X5T);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,7 +122,7 @@ public final class JoseHeader {
|
|||
* @return the X.509 certificate SHA-256 thumbprint
|
||||
*/
|
||||
public String getX509SHA256Thumbprint() {
|
||||
return getHeader(X5T_S256);
|
||||
return getHeader(JoseHeaderNames.X5T_S256);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -143,7 +132,7 @@ public final class JoseHeader {
|
|||
* @return the critical headers
|
||||
*/
|
||||
public Set<String> getCritical() {
|
||||
return getHeader(CRIT);
|
||||
return getHeader(JoseHeaderNames.CRIT);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -152,7 +141,7 @@ public final class JoseHeader {
|
|||
* @return the type header
|
||||
*/
|
||||
public String getType() {
|
||||
return getHeader(TYP);
|
||||
return getHeader(JoseHeaderNames.TYP);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -161,7 +150,7 @@ public final class JoseHeader {
|
|||
* @return the content type header
|
||||
*/
|
||||
public String getContentType() {
|
||||
return getHeader(CTY);
|
||||
return getHeader(JoseHeaderNames.CTY);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -186,6 +175,15 @@ public final class JoseHeader {
|
|||
return (T) getHeaders().get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}.
|
||||
*
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@link JwsAlgorithm}.
|
||||
*
|
||||
|
@ -209,12 +207,15 @@ public final class JoseHeader {
|
|||
/**
|
||||
* A builder for {@link JoseHeader}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final Map<String, Object> headers = new LinkedHashMap<>();
|
||||
public static final class Builder {
|
||||
private final Map<String, Object> headers = new HashMap<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
private Builder(JwsAlgorithm jwsAlgorithm) {
|
||||
Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
|
||||
header(ALG, jwsAlgorithm);
|
||||
header(JoseHeaderNames.ALG, jwsAlgorithm);
|
||||
}
|
||||
|
||||
private Builder(JoseHeader headers) {
|
||||
|
@ -222,6 +223,16 @@ public final class JoseHeader {
|
|||
this.headers.putAll(headers.getHeaders());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link JwsAlgorithm JWS algorithm} used to digitally sign the JWS.
|
||||
*
|
||||
* @param jwsAlgorithm the JWS algorithm
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder jwsAlgorithm(JwsAlgorithm jwsAlgorithm) {
|
||||
return header(JoseHeaderNames.ALG, jwsAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -230,7 +241,7 @@ public final class JoseHeader {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder jwkSetUri(String jwkSetUri) {
|
||||
return header(JKU, jwkSetUri);
|
||||
return header(JoseHeaderNames.JKU, jwkSetUri);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -241,7 +252,7 @@ public final class JoseHeader {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder jwk(Map<String, Object> jwk) {
|
||||
return header(JWK, jwk);
|
||||
return header(JoseHeaderNames.JWK, jwk);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -251,7 +262,7 @@ public final class JoseHeader {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder keyId(String keyId) {
|
||||
return header(KID, keyId);
|
||||
return header(JoseHeaderNames.KID, keyId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -262,7 +273,7 @@ public final class JoseHeader {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509Uri(String x509Uri) {
|
||||
return header(X5U, x509Uri);
|
||||
return header(JoseHeaderNames.X5U, x509Uri);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -273,7 +284,7 @@ public final class JoseHeader {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509CertificateChain(List<String> x509CertificateChain) {
|
||||
return header(X5C, x509CertificateChain);
|
||||
return header(JoseHeaderNames.X5C, x509CertificateChain);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -284,7 +295,7 @@ public final class JoseHeader {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509SHA1Thumbprint(String x509SHA1Thumbprint) {
|
||||
return header(X5T, x509SHA1Thumbprint);
|
||||
return header(JoseHeaderNames.X5T, x509SHA1Thumbprint);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -295,7 +306,7 @@ public final class JoseHeader {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder x509SHA256Thumbprint(String x509SHA256Thumbprint) {
|
||||
return header(X5T_S256, x509SHA256Thumbprint);
|
||||
return header(JoseHeaderNames.X5T_S256, x509SHA256Thumbprint);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -306,7 +317,7 @@ public final class JoseHeader {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder critical(Set<String> headerNames) {
|
||||
return header(CRIT, headerNames);
|
||||
return header(JoseHeaderNames.CRIT, headerNames);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -316,7 +327,7 @@ public final class JoseHeader {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder type(String type) {
|
||||
return header(TYP, type);
|
||||
return header(JoseHeaderNames.TYP, type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -326,7 +337,7 @@ public final class JoseHeader {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder contentType(String contentType) {
|
||||
return header(CTY, contentType);
|
||||
return header(JoseHeaderNames.CTY, contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -362,7 +373,19 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
/**
|
||||
* The Registered Header Parameter Names defined by the JSON Web Token (JWT),
|
||||
|
@ -28,69 +28,72 @@ package org.springframework.security.oauth2.jose;
|
|||
* @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>
|
||||
*/
|
||||
public interface JoseHeaderNames {
|
||||
public final class JoseHeaderNames {
|
||||
|
||||
/**
|
||||
* {@code alg} - the algorithm header identifies the cryptographic algorithm used to secure a JWS or JWE
|
||||
*/
|
||||
String ALG = "alg";
|
||||
public static final String ALG = "alg";
|
||||
|
||||
/**
|
||||
* {@code jku} - the JWK Set URL header is a URI that refers to a resource for a set of JSON-encoded public keys,
|
||||
* one of which corresponds to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String JKU = "jku";
|
||||
public static final String JKU = "jku";
|
||||
|
||||
/**
|
||||
* {@code jwk} - the JSON Web Key header is the public key that corresponds to the key
|
||||
* used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String JWK = "jwk";
|
||||
public static final String JWK = "jwk";
|
||||
|
||||
/**
|
||||
* {@code kid} - the key ID header is a hint indicating which key was used to secure a JWS or JWE
|
||||
*/
|
||||
String KID = "kid";
|
||||
public static final String KID = "kid";
|
||||
|
||||
/**
|
||||
* {@code x5u} - the X.509 URL header is a URI that refers to a resource for the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5U = "x5u";
|
||||
public static final String X5U = "x5u";
|
||||
|
||||
/**
|
||||
* {@code x5c} - the X.509 certificate chain header contains the X.509 public key certificate
|
||||
* or certificate chain corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5C = "x5c";
|
||||
public static final String X5C = "x5c";
|
||||
|
||||
/**
|
||||
* {@code x5t} - the X.509 certificate SHA-1 thumbprint header is a base64url-encoded SHA-1 thumbprint (a.k.a. digest)
|
||||
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5T = "x5t";
|
||||
public static final String X5T = "x5t";
|
||||
|
||||
/**
|
||||
* {@code x5t#S256} - the X.509 certificate SHA-256 thumbprint header is a base64url-encoded SHA-256 thumbprint (a.k.a. digest)
|
||||
* of the DER encoding of the X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
String X5T_S256 = "x5t#S256";
|
||||
public static final String X5T_S256 = "x5t#S256";
|
||||
|
||||
/**
|
||||
* {@code typ} - the type header is used by JWS/JWE applications to declare the media type of a JWS/JWE
|
||||
*/
|
||||
String TYP = "typ";
|
||||
public static final String TYP = "typ";
|
||||
|
||||
/**
|
||||
* {@code cty} - the content type header is used by JWS/JWE applications to declare the media type
|
||||
* of the secured content (the payload)
|
||||
*/
|
||||
String CTY = "cty";
|
||||
public static final String CTY = "cty";
|
||||
|
||||
/**
|
||||
* {@code crit} - the critical header indicates that extensions to the JWS/JWE/JWA specifications
|
||||
* are being used that MUST be understood and processed
|
||||
*/
|
||||
String CRIT = "crit";
|
||||
public static final String CRIT = "crit";
|
||||
|
||||
private JoseHeaderNames() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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,23 +15,14 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The {@link Jwt JWT} Claims Set is a JSON object representing the claims conveyed by a JSON Web Token.
|
||||
|
@ -47,7 +38,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
private final Map<String, Object> claims;
|
||||
|
||||
private JwtClaimsSet(Map<String, Object> claims) {
|
||||
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
|
||||
this.claims = Collections.unmodifiableMap(new HashMap<>(claims));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -60,7 +51,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
*
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withClaims() {
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
|
@ -77,8 +68,8 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
/**
|
||||
* A builder for {@link JwtClaimsSet}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final Map<String, Object> claims = new LinkedHashMap<>();
|
||||
public static final class Builder {
|
||||
private final Map<String, Object> claims = new HashMap<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
@ -94,8 +85,8 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @param issuer the issuer identifier
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder issuer(URL issuer) {
|
||||
return claim(ISS, issuer);
|
||||
public Builder issuer(String issuer) {
|
||||
return claim(JwtClaimNames.ISS, issuer);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,7 +96,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder subject(String subject) {
|
||||
return claim(SUB, subject);
|
||||
return claim(JwtClaimNames.SUB, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,7 +106,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder audience(List<String> audience) {
|
||||
return claim(AUD, audience);
|
||||
return claim(JwtClaimNames.AUD, audience);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,7 +117,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder expiresAt(Instant expiresAt) {
|
||||
return claim(EXP, expiresAt);
|
||||
return claim(JwtClaimNames.EXP, expiresAt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,7 +128,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder notBefore(Instant notBefore) {
|
||||
return claim(NBF, notBefore);
|
||||
return claim(JwtClaimNames.NBF, notBefore);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -147,7 +138,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder issuedAt(Instant issuedAt) {
|
||||
return claim(IAT, issuedAt);
|
||||
return claim(JwtClaimNames.IAT, issuedAt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -157,7 +148,7 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder id(String jti) {
|
||||
return claim(JTI, jti);
|
||||
return claim(JwtClaimNames.JTI, jti);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,8 +15,6 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.springframework.security.oauth2.jose.JoseHeader;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for encoding
|
||||
* a JSON Web Token (JWT) to it's compact claims representation format.
|
||||
|
|
|
@ -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,53 +13,47 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.jose.jws;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
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.KeyLengthException;
|
||||
import com.nimbusds.jose.crypto.MACSigner;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
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.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import com.nimbusds.jose.produce.JWSSignerFactory;
|
||||
import com.nimbusds.jose.util.Base64;
|
||||
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.security.crypto.keys.KeyManager;
|
||||
import org.springframework.security.crypto.keys.ManagedKey;
|
||||
import org.springframework.security.oauth2.jose.JoseHeader;
|
||||
import org.springframework.security.oauth2.jose.JoseHeaderNames;
|
||||
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.jwt.JwtEncodingException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.PrivateKey;
|
||||
import java.time.Instant;
|
||||
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.stream.Collectors;
|
||||
|
||||
/**
|
||||
* An implementation of a {@link JwtEncoder} that encodes a JSON Web Token (JWT)
|
||||
* using the JSON Web Signature (JWS) Compact Serialization format.
|
||||
* The private/secret key used for signing the JWS is obtained
|
||||
* from the {@link KeyManager} supplied via the constructor.
|
||||
* An implementation of a {@link JwtEncoder} that encodes a JSON Web Token (JWT) using the
|
||||
* JSON Web Signature (JWS) Compact Serialization format. The private/secret key used for
|
||||
* signing the JWS is supplied by the {@code com.nimbusds.jose.jwk.source.JWKSource}
|
||||
* provided via the constructor.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK.
|
||||
|
@ -67,42 +61,38 @@ import java.util.stream.Collectors;
|
|||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see JwtEncoder
|
||||
* @see KeyManager
|
||||
* @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 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>
|
||||
*/
|
||||
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 String RSA_KEY_TYPE = "RSA";
|
||||
private static final String EC_KEY_TYPE = "EC";
|
||||
private static final Map<JwsAlgorithm, String> jcaKeyAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
|
||||
{
|
||||
put(MacAlgorithm.HS256, "HmacSHA256");
|
||||
put(MacAlgorithm.HS384, "HmacSHA384");
|
||||
put(MacAlgorithm.HS512, "HmacSHA512");
|
||||
put(SignatureAlgorithm.RS256, RSA_KEY_TYPE);
|
||||
put(SignatureAlgorithm.RS384, RSA_KEY_TYPE);
|
||||
put(SignatureAlgorithm.RS512, RSA_KEY_TYPE);
|
||||
put(SignatureAlgorithm.ES256, EC_KEY_TYPE);
|
||||
put(SignatureAlgorithm.ES384, EC_KEY_TYPE);
|
||||
put(SignatureAlgorithm.ES512, EC_KEY_TYPE);
|
||||
}
|
||||
};
|
||||
private static final Converter<JoseHeader, JWSHeader> jwsHeaderConverter = new JwsHeaderConverter();
|
||||
private static final Converter<JwtClaimsSet, JWTClaimsSet> jwtClaimsSetConverter = new JwtClaimsSetConverter();
|
||||
private final KeyManager keyManager;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Constructs a {@code NimbusJwsEncoder} using the provided parameters.
|
||||
*
|
||||
* @param keyManager the key manager
|
||||
* @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
|
||||
*/
|
||||
public NimbusJwsEncoder(KeyManager keyManager) {
|
||||
Assert.notNull(keyManager, "keyManager cannot be null");
|
||||
this.keyManager = keyManager;
|
||||
public NimbusJwsEncoder(JWKSource<SecurityContext> jwkSource) {
|
||||
Assert.notNull(jwkSource, "jwkSource cannot be null");
|
||||
this.jwkSource = jwkSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -110,84 +100,79 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
|||
Assert.notNull(headers, "headers cannot be null");
|
||||
Assert.notNull(claims, "claims cannot be null");
|
||||
|
||||
ManagedKey managedKey = selectKey(headers);
|
||||
if (managedKey == null) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Unsupported key for algorithm '" + headers.getJwsAlgorithm().getName() + "'"));
|
||||
}
|
||||
|
||||
JWSSigner jwsSigner;
|
||||
if (managedKey.isAsymmetric()) {
|
||||
if (!managedKey.getAlgorithm().equals(RSA_KEY_TYPE)) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Unsupported key type '" + managedKey.getAlgorithm() + "'"));
|
||||
}
|
||||
PrivateKey privateKey = managedKey.getKey();
|
||||
jwsSigner = new RSASSASigner(privateKey);
|
||||
} else {
|
||||
SecretKey secretKey = managedKey.getKey();
|
||||
try {
|
||||
jwsSigner = new MACSigner(secretKey);
|
||||
} catch (KeyLengthException ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
|
||||
}
|
||||
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"));
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
headers = JoseHeader.from(headers)
|
||||
.type(JOSEObjectType.JWT.getType())
|
||||
.keyId(managedKey.getKeyId())
|
||||
.keyId(jwk.getKeyID())
|
||||
.build();
|
||||
JWSHeader jwsHeader = jwsHeaderConverter.convert(headers);
|
||||
|
||||
claims = JwtClaimsSet.from(claims)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
JWTClaimsSet jwtClaimsSet = jwtClaimsSetConverter.convert(claims);
|
||||
// @formatter:on
|
||||
|
||||
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaimsSet);
|
||||
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, ex.getMessage()), ex);
|
||||
signedJwt.sign(jwsSigner);
|
||||
}
|
||||
String jws = signedJWT.serialize();
|
||||
catch (JOSEException ex) {
|
||||
throw new JwtEncodingException(
|
||||
String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to sign the JWT -> " + ex.getMessage()), ex);
|
||||
}
|
||||
String jws = signedJwt.serialize();
|
||||
|
||||
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(),
|
||||
headers.getHeaders(), claims.getClaims());
|
||||
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(), headers.getHeaders(), claims.getClaims());
|
||||
}
|
||||
|
||||
private ManagedKey selectKey(JoseHeader headers) {
|
||||
JwsAlgorithm jwsAlgorithm = headers.getJwsAlgorithm();
|
||||
String keyAlgorithm = jcaKeyAlgorithmMappings.get(jwsAlgorithm);
|
||||
if (!StringUtils.hasText(keyAlgorithm)) {
|
||||
return null;
|
||||
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 {
|
||||
jwks = this.jwkSource.get(jwkSelector, null);
|
||||
}
|
||||
catch (KeySourceException ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to select a JWK signing key -> " + ex.getMessage()), ex);
|
||||
}
|
||||
|
||||
Set<ManagedKey> matchingKeys = this.keyManager.findByAlgorithm(keyAlgorithm);
|
||||
if (CollectionUtils.isEmpty(matchingKeys)) {
|
||||
return null;
|
||||
if (jwks.size() > 1) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Found multiple JWK signing keys for algorithm '" + jwsAlgorithm.getName() + "'"));
|
||||
}
|
||||
|
||||
return matchingKeys.stream()
|
||||
.filter(ManagedKey::isActive)
|
||||
.max(this::mostRecentActivated)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private int mostRecentActivated(ManagedKey managedKey1, ManagedKey managedKey2) {
|
||||
return managedKey1.getActivatedOn().isAfter(managedKey2.getActivatedOn()) ? 1 : -1;
|
||||
return !jwks.isEmpty() ? jwks.get(0) : null;
|
||||
}
|
||||
|
||||
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.getJwsAlgorithm().getName()));
|
||||
|
||||
Set<String> critical = headers.getCritical();
|
||||
if (!CollectionUtils.isEmpty(critical)) {
|
||||
|
@ -199,14 +184,14 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
|||
builder.contentType(contentType);
|
||||
}
|
||||
|
||||
String jwkSetUri = headers.getJwkSetUri();
|
||||
if (StringUtils.hasText(jwkSetUri)) {
|
||||
URL jwkSetUri = headers.getJwkSetUri();
|
||||
if (jwkSetUri != null) {
|
||||
try {
|
||||
builder.jwkURL(new URI(jwkSetUri));
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.JKU + "' JOSE header"), ex);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -214,9 +199,9 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
|||
if (!CollectionUtils.isEmpty(jwk)) {
|
||||
try {
|
||||
builder.jwk(JWK.parse(new JSONObject(jwk)));
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex);
|
||||
}
|
||||
}
|
||||
|
@ -233,10 +218,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
|||
|
||||
List<String> x509CertificateChain = headers.getX509CertificateChain();
|
||||
if (!CollectionUtils.isEmpty(x509CertificateChain)) {
|
||||
builder.x509CertChain(
|
||||
x509CertificateChain.stream()
|
||||
.map(Base64::new)
|
||||
.collect(Collectors.toList()));
|
||||
builder.x509CertChain(x509CertificateChain.stream().map(Base64::new).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
String x509SHA1Thumbprint = headers.getX509SHA1Thumbprint();
|
||||
|
@ -249,19 +231,19 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
|||
builder.x509CertSHA256Thumbprint(new Base64URL(x509SHA256Thumbprint));
|
||||
}
|
||||
|
||||
String x509Uri = headers.getX509Uri();
|
||||
if (StringUtils.hasText(x509Uri)) {
|
||||
URL x509Uri = headers.getX509Uri();
|
||||
if (x509Uri != null) {
|
||||
try {
|
||||
builder.x509CertURL(new URI(x509Uri));
|
||||
} catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(
|
||||
ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to convert '" + JoseHeaderNames.X5U + "' JOSE header"), ex);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> customHeaders = headers.getHeaders().entrySet().stream()
|
||||
.filter(header -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey()))
|
||||
.filter((header) -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
if (!CollectionUtils.isEmpty(customHeaders)) {
|
||||
builder.customParams(customHeaders);
|
||||
|
@ -269,6 +251,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
|||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class JwtClaimsSetConverter implements Converter<JwtClaimsSet, JWTClaimsSet> {
|
||||
|
@ -313,7 +296,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
|||
}
|
||||
|
||||
Map<String, Object> customClaims = claims.getClaims().entrySet().stream()
|
||||
.filter(claim -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey()))
|
||||
.filter((claim) -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
if (!CollectionUtils.isEmpty(customClaims)) {
|
||||
customClaims.forEach(builder::claim);
|
||||
|
@ -321,5 +304,7 @@ public final class NimbusJwsEncoder implements JwtEncoder {
|
|||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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,36 +15,86 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion2;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.Serializable;
|
||||
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.security.oauth2.core.OAuth2AccessToken;
|
||||
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.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2AuthorizationService} that stores {@link OAuth2Authorization}'s in-memory.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
|
||||
*
|
||||
* @author Krisztian Toth
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see OAuth2AuthorizationService
|
||||
*/
|
||||
public final class InMemoryOAuth2AuthorizationService implements OAuth2AuthorizationService {
|
||||
private final Map<OAuth2AuthorizationId, OAuth2Authorization> authorizations = new ConcurrentHashMap<>();
|
||||
private final Map<String, OAuth2Authorization> authorizations = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs an {@code InMemoryOAuth2AuthorizationService}.
|
||||
*/
|
||||
public InMemoryOAuth2AuthorizationService() {
|
||||
this(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code InMemoryOAuth2AuthorizationService} using the provided parameters.
|
||||
*
|
||||
* @param authorizations the authorization(s)
|
||||
*/
|
||||
public InMemoryOAuth2AuthorizationService(OAuth2Authorization... authorizations) {
|
||||
this(Arrays.asList(authorizations));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code InMemoryOAuth2AuthorizationService} using the provided parameters.
|
||||
*
|
||||
* @param authorizations the authorization(s)
|
||||
*/
|
||||
public InMemoryOAuth2AuthorizationService(List<OAuth2Authorization> authorizations) {
|
||||
Assert.notNull(authorizations, "authorizations cannot be null");
|
||||
authorizations.forEach(authorization -> {
|
||||
Assert.notNull(authorization, "authorization cannot be null");
|
||||
Assert.isTrue(!this.authorizations.containsKey(authorization.getId()),
|
||||
"The authorization must be unique. Found duplicate identifier: " + authorization.getId());
|
||||
this.authorizations.put(authorization.getId(), authorization);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(OAuth2Authorization authorization) {
|
||||
Assert.notNull(authorization, "authorization cannot be null");
|
||||
OAuth2AuthorizationId authorizationId = new OAuth2AuthorizationId(
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName());
|
||||
this.authorizations.put(authorizationId, authorization);
|
||||
this.authorizations.put(authorization.getId(), authorization);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2Authorization findByToken(String token, @Nullable TokenType tokenType) {
|
||||
public void remove(OAuth2Authorization authorization) {
|
||||
Assert.notNull(authorization, "authorization cannot be null");
|
||||
this.authorizations.remove(authorization.getId(), authorization);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2Authorization findById(String id) {
|
||||
Assert.hasText(id, "id cannot be empty");
|
||||
return this.authorizations.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))
|
||||
|
@ -52,42 +102,43 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
|
|||
.orElse(null);
|
||||
}
|
||||
|
||||
private boolean hasToken(OAuth2Authorization authorization, String token, TokenType tokenType) {
|
||||
if (TokenType.AUTHORIZATION_CODE.equals(tokenType)) {
|
||||
return token.equals(authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE));
|
||||
} else if (TokenType.ACCESS_TOKEN.equals(tokenType)) {
|
||||
return authorization.getAccessToken() != null &&
|
||||
authorization.getAccessToken().getTokenValue().equals(token);
|
||||
private static boolean hasToken(OAuth2Authorization authorization, String token, @Nullable OAuth2TokenType tokenType) {
|
||||
if (tokenType == null) {
|
||||
return matchesState(authorization, token) ||
|
||||
matchesAuthorizationCode(authorization, token) ||
|
||||
matchesAccessToken(authorization, token) ||
|
||||
matchesRefreshToken(authorization, token);
|
||||
} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
|
||||
return matchesState(authorization, token);
|
||||
} else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
|
||||
return matchesAuthorizationCode(authorization, token);
|
||||
} else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
|
||||
return matchesAccessToken(authorization, token);
|
||||
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
|
||||
return matchesRefreshToken(authorization, token);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class OAuth2AuthorizationId implements Serializable {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
private final String registeredClientId;
|
||||
private final String principalName;
|
||||
private static boolean matchesState(OAuth2Authorization authorization, String token) {
|
||||
return token.equals(authorization.getAttribute(OAuth2ParameterNames.STATE));
|
||||
}
|
||||
|
||||
private OAuth2AuthorizationId(String registeredClientId, String principalName) {
|
||||
this.registeredClientId = registeredClientId;
|
||||
this.principalName = principalName;
|
||||
}
|
||||
private static boolean matchesAuthorizationCode(OAuth2Authorization authorization, String token) {
|
||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
|
||||
authorization.getToken(OAuth2AuthorizationCode.class);
|
||||
return authorizationCode != null && authorizationCode.getToken().getTokenValue().equals(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
OAuth2AuthorizationId that = (OAuth2AuthorizationId) obj;
|
||||
return Objects.equals(this.registeredClientId, that.registeredClientId) &&
|
||||
Objects.equals(this.principalName, that.principalName);
|
||||
}
|
||||
private static boolean matchesAccessToken(OAuth2Authorization authorization, String token) {
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> accessToken =
|
||||
authorization.getToken(OAuth2AccessToken.class);
|
||||
return accessToken != null && accessToken.getToken().getTokenValue().equals(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.registeredClientId, this.principalName);
|
||||
}
|
||||
private static boolean matchesRefreshToken(OAuth2Authorization authorization, String token) {
|
||||
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken =
|
||||
authorization.getToken(OAuth2RefreshToken.class);
|
||||
return refreshToken != null && refreshToken.getToken().getTokenValue().equals(token);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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.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.JwtClaimsSet;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see OAuth2TokenContext
|
||||
* @see JoseHeader.Builder
|
||||
* @see JwtClaimsSet.Builder
|
||||
*/
|
||||
public final class JwtEncodingContext implements OAuth2TokenContext {
|
||||
private final Context context;
|
||||
|
||||
private JwtEncodingContext(Map<Object, Object> context) {
|
||||
this.context = Context.of(context);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return this.context.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
return this.context.hasKey(key);
|
||||
}
|
||||
|
||||
public JoseHeader.Builder getHeaders() {
|
||||
return get(JoseHeader.Builder.class);
|
||||
}
|
||||
|
||||
public JwtClaimsSet.Builder getClaims() {
|
||||
return get(JwtClaimsSet.Builder.class);
|
||||
}
|
||||
|
||||
public static Builder with(JoseHeader.Builder headersBuilder, JwtClaimsSet.Builder claimsBuilder) {
|
||||
return new Builder(headersBuilder, claimsBuilder);
|
||||
}
|
||||
|
||||
public static final class Builder extends AbstractBuilder<JwtEncodingContext, Builder> {
|
||||
|
||||
private Builder(JoseHeader.Builder headersBuilder, JwtClaimsSet.Builder claimsBuilder) {
|
||||
Assert.notNull(headersBuilder, "headersBuilder cannot be null");
|
||||
Assert.notNull(claimsBuilder, "claimsBuilder cannot be null");
|
||||
put(JoseHeader.Builder.class, headersBuilder);
|
||||
put(JwtClaimsSet.Builder.class, claimsBuilder);
|
||||
}
|
||||
|
||||
public Builder headers(Consumer<JoseHeader.Builder> headersConsumer) {
|
||||
headersConsumer.accept(get(JoseHeader.Builder.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder claims(Consumer<JwtClaimsSet.Builder> claimsConsumer) {
|
||||
claimsConsumer.accept(get(JwtClaimsSet.Builder.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
public JwtEncodingContext build() {
|
||||
return new JwtEncodingContext(this.context);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,39 +15,68 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion2;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
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.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A representation of an OAuth 2.0 Authorization,
|
||||
* which holds state related to the authorization granted to the {@link #getRegisteredClientId() client}
|
||||
* by the {@link #getPrincipalName() resource owner}.
|
||||
* A representation of an OAuth 2.0 Authorization, which holds state related to the authorization granted
|
||||
* to a {@link #getRegisteredClientId() client}, by the {@link #getPrincipalName() resource owner}
|
||||
* or itself in the case of the {@code client_credentials} grant type.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Krisztian Toth
|
||||
* @since 0.0.1
|
||||
* @see RegisteredClient
|
||||
* @see AuthorizationGrantType
|
||||
* @see AbstractOAuth2Token
|
||||
* @see OAuth2AccessToken
|
||||
* @see OAuth2RefreshToken
|
||||
*/
|
||||
public class OAuth2Authorization implements Serializable {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
|
||||
/**
|
||||
* The name of the {@link #getAttribute(String) attribute} used for the authorized scope(s).
|
||||
* The value of the attribute is of type {@code Set<String>}.
|
||||
*/
|
||||
public static final String AUTHORIZED_SCOPE_ATTRIBUTE_NAME =
|
||||
OAuth2Authorization.class.getName().concat(".AUTHORIZED_SCOPE");
|
||||
|
||||
private String id;
|
||||
private String registeredClientId;
|
||||
private String principalName;
|
||||
private OAuth2AccessToken accessToken;
|
||||
private AuthorizationGrantType authorizationGrantType;
|
||||
private Map<Class<? extends AbstractOAuth2Token>, Token<?>> tokens;
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
protected OAuth2Authorization() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier for the authorization.
|
||||
*
|
||||
* @return the identifier for the authorization
|
||||
*/
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier for the {@link RegisteredClient#getId() registered client}.
|
||||
*
|
||||
|
@ -58,21 +87,73 @@ public class OAuth2Authorization implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the resource owner's {@code Principal} name.
|
||||
* Returns the {@code Principal} name of the resource owner (or client).
|
||||
*
|
||||
* @return the resource owner's {@code Principal} name
|
||||
* @return the {@code Principal} name of the resource owner (or client)
|
||||
*/
|
||||
public String getPrincipalName() {
|
||||
return this.principalName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2AccessToken access token} credential.
|
||||
* Returns the {@link AuthorizationGrantType authorization grant type} used for the authorization.
|
||||
*
|
||||
* @return the {@link OAuth2AccessToken}
|
||||
* @return the {@link AuthorizationGrantType} used for the authorization
|
||||
*/
|
||||
public OAuth2AccessToken getAccessToken() {
|
||||
return this.accessToken;
|
||||
public AuthorizationGrantType getAuthorizationGrantType() {
|
||||
return this.authorizationGrantType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Token} of type {@link OAuth2AccessToken}.
|
||||
*
|
||||
* @return the {@link Token} of type {@link OAuth2AccessToken}
|
||||
*/
|
||||
public Token<OAuth2AccessToken> getAccessToken() {
|
||||
return getToken(OAuth2AccessToken.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Token} of type {@link OAuth2RefreshToken}.
|
||||
*
|
||||
* @return the {@link Token} of type {@link OAuth2RefreshToken}, or {@code null} if not available
|
||||
*/
|
||||
@Nullable
|
||||
public Token<OAuth2RefreshToken> getRefreshToken() {
|
||||
return getToken(OAuth2RefreshToken.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Token} of type {@code tokenType}.
|
||||
*
|
||||
* @param tokenType the token type
|
||||
* @param <T> the type of the token
|
||||
* @return the {@link Token}, or {@code null} if not available
|
||||
*/
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends AbstractOAuth2Token> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Token} matching the {@code tokenValue}.
|
||||
*
|
||||
* @param tokenValue the token value
|
||||
* @param <T> the type of the token
|
||||
* @return the {@link Token}, or {@code null} if not available
|
||||
*/
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends AbstractOAuth2Token> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,8 +170,9 @@ public class OAuth2Authorization implements Serializable {
|
|||
*
|
||||
* @param name the name of the attribute
|
||||
* @param <T> the type of the attribute
|
||||
* @return the value of the attribute associated to the authorization, or {@code null} if not available
|
||||
* @return the value of an attribute associated to the authorization, or {@code null} if not available
|
||||
*/
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getAttribute(String name) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
|
@ -106,15 +188,18 @@ public class OAuth2Authorization implements Serializable {
|
|||
return false;
|
||||
}
|
||||
OAuth2Authorization that = (OAuth2Authorization) obj;
|
||||
return Objects.equals(this.registeredClientId, that.registeredClientId) &&
|
||||
return Objects.equals(this.id, that.id) &&
|
||||
Objects.equals(this.registeredClientId, that.registeredClientId) &&
|
||||
Objects.equals(this.principalName, that.principalName) &&
|
||||
Objects.equals(this.accessToken, that.accessToken) &&
|
||||
Objects.equals(this.authorizationGrantType, that.authorizationGrantType) &&
|
||||
Objects.equals(this.tokens, that.tokens) &&
|
||||
Objects.equals(this.attributes, that.attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.registeredClientId, this.principalName, this.accessToken, this.attributes);
|
||||
return Objects.hash(this.id, this.registeredClientId, this.principalName,
|
||||
this.authorizationGrantType, this.tokens, this.attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -129,37 +214,161 @@ public class OAuth2Authorization implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the values from the provided {@code authorization}.
|
||||
* Returns a new {@link Builder}, initialized with the values from the provided {@code OAuth2Authorization}.
|
||||
*
|
||||
* @param authorization the authorization used for initializing the {@link Builder}
|
||||
* @param authorization the {@code OAuth2Authorization} used for initializing the {@link Builder}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder from(OAuth2Authorization authorization) {
|
||||
Assert.notNull(authorization, "authorization cannot be null");
|
||||
return new Builder(authorization.getRegisteredClientId())
|
||||
.id(authorization.getId())
|
||||
.principalName(authorization.getPrincipalName())
|
||||
.accessToken(authorization.getAccessToken())
|
||||
.authorizationGrantType(authorization.getAuthorizationGrantType())
|
||||
.tokens(authorization.tokens)
|
||||
.attributes(attrs -> attrs.putAll(authorization.getAttributes()));
|
||||
}
|
||||
|
||||
/**
|
||||
* A holder of an OAuth 2.0 Token and it's associated metadata.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
*/
|
||||
public static class Token<T extends AbstractOAuth2Token> implements Serializable {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
protected static final String TOKEN_METADATA_BASE = "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");
|
||||
|
||||
/**
|
||||
* The name of the metadata used for the claims of the token.
|
||||
*/
|
||||
public static final String CLAIMS_METADATA_NAME = TOKEN_METADATA_BASE.concat("claims");
|
||||
|
||||
private final T token;
|
||||
private final Map<String, Object> metadata;
|
||||
|
||||
protected Token(T token) {
|
||||
this(token, defaultMetadata());
|
||||
}
|
||||
|
||||
protected Token(T token, Map<String, Object> metadata) {
|
||||
this.token = token;
|
||||
this.metadata = Collections.unmodifiableMap(metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the token of type {@link AbstractOAuth2Token}.
|
||||
*
|
||||
* @return the token of type {@link AbstractOAuth2Token}
|
||||
*/
|
||||
public T getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the token has been invalidated (e.g. revoked).
|
||||
* The default is {@code false}.
|
||||
*
|
||||
* @return {@code true} if the token has been invalidated, {@code false} otherwise
|
||||
*/
|
||||
public boolean isInvalidated() {
|
||||
return Boolean.TRUE.equals(getMetadata(INVALIDATED_METADATA_NAME));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the claims associated to the token.
|
||||
*
|
||||
* @return a {@code Map} of the claims, or {@code null} if not available
|
||||
*/
|
||||
@Nullable
|
||||
public Map<String, Object> getClaims() {
|
||||
return getMetadata(CLAIMS_METADATA_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the metadata associated to the token.
|
||||
*
|
||||
* @param name the name of the metadata
|
||||
* @param <V> the value type of the metadata
|
||||
* @return the value of the metadata, or {@code null} if not available
|
||||
*/
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
public <V> V getMetadata(String name) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
return (V) this.metadata.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the metadata associated to the token.
|
||||
*
|
||||
* @return a {@code Map} of the metadata
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
protected static Map<String, Object> defaultMetadata() {
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put(INVALIDATED_METADATA_NAME, false);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Token<?> that = (Token<?>) obj;
|
||||
return Objects.equals(this.token, that.token) &&
|
||||
Objects.equals(this.metadata, that.metadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.token, this.metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OAuth2Authorization}.
|
||||
*/
|
||||
public static class Builder implements Serializable {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
private String registeredClientId;
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private String id;
|
||||
private final String registeredClientId;
|
||||
private String principalName;
|
||||
private OAuth2AccessToken accessToken;
|
||||
private Map<String, Object> attributes = new HashMap<>();
|
||||
private AuthorizationGrantType authorizationGrantType;
|
||||
private Map<Class<? extends AbstractOAuth2Token>, Token<?>> tokens = new HashMap<>();
|
||||
private final Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
protected Builder(String registeredClientId) {
|
||||
this.registeredClientId = registeredClientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resource owner's {@code Principal} name.
|
||||
* Sets the identifier for the authorization.
|
||||
*
|
||||
* @param principalName the resource owner's {@code Principal} name
|
||||
* @param id the identifier for the authorization
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder id(String id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Principal} name of the resource owner (or client).
|
||||
*
|
||||
* @param principalName the {@code Principal} name of the resource owner (or client)
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder principalName(String principalName) {
|
||||
|
@ -168,13 +377,75 @@ public class OAuth2Authorization implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2AccessToken access token} credential.
|
||||
* Sets the {@link AuthorizationGrantType authorization grant type} used for the authorization.
|
||||
*
|
||||
* @param authorizationGrantType the {@link AuthorizationGrantType}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder authorizationGrantType(AuthorizationGrantType authorizationGrantType) {
|
||||
this.authorizationGrantType = authorizationGrantType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2AccessToken access token}.
|
||||
*
|
||||
* @param accessToken the {@link OAuth2AccessToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder accessToken(OAuth2AccessToken accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
return token(accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2RefreshToken refresh token}.
|
||||
*
|
||||
* @param refreshToken the {@link OAuth2RefreshToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder refreshToken(OAuth2RefreshToken refreshToken) {
|
||||
return token(refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AbstractOAuth2Token token}.
|
||||
*
|
||||
* @param token the token
|
||||
* @param <T> the type of the token
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public <T extends AbstractOAuth2Token> Builder token(T token) {
|
||||
return token(token, (metadata) -> {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AbstractOAuth2Token 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,
|
||||
Consumer<Map<String, Object>> metadataConsumer) {
|
||||
|
||||
Assert.notNull(token, "token cannot be null");
|
||||
Map<String, Object> metadata = Token.defaultMetadata();
|
||||
Token<?> existingToken = this.tokens.get(token.getClass());
|
||||
if (existingToken != null) {
|
||||
metadata.putAll(existingToken.getMetadata());
|
||||
}
|
||||
metadataConsumer.accept(metadata);
|
||||
Class<? extends AbstractOAuth2Token> tokenClass = token.getClass();
|
||||
if (tokenClass.equals(OAuth2RefreshToken2.class)) {
|
||||
tokenClass = OAuth2RefreshToken.class;
|
||||
}
|
||||
this.tokens.put(tokenClass, new Token<>(token, metadata));
|
||||
return this;
|
||||
}
|
||||
|
||||
protected final Builder tokens(Map<Class<? extends AbstractOAuth2Token>, Token<?>> tokens) {
|
||||
this.tokens = new HashMap<>(tokens);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -211,11 +482,17 @@ public class OAuth2Authorization implements Serializable {
|
|||
*/
|
||||
public OAuth2Authorization build() {
|
||||
Assert.hasText(this.principalName, "principalName cannot be empty");
|
||||
Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null");
|
||||
|
||||
OAuth2Authorization authorization = new OAuth2Authorization();
|
||||
if (!StringUtils.hasText(this.id)) {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
}
|
||||
authorization.id = this.id;
|
||||
authorization.registeredClientId = this.registeredClientId;
|
||||
authorization.principalName = this.principalName;
|
||||
authorization.accessToken = this.accessToken;
|
||||
authorization.authorizationGrantType = this.authorizationGrantType;
|
||||
authorization.tokens = Collections.unmodifiableMap(this.tokens);
|
||||
authorization.attributes = Collections.unmodifiableMap(this.attributes);
|
||||
return authorization;
|
||||
}
|
||||
|
|
|
@ -1,48 +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;
|
||||
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
|
||||
/**
|
||||
* The name of the attributes that may be contained in the
|
||||
* {@link OAuth2Authorization#getAttributes()} {@code Map}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see OAuth2Authorization#getAttributes()
|
||||
*/
|
||||
public interface OAuth2AuthorizationAttributeNames {
|
||||
|
||||
/**
|
||||
* The name of the attribute used for the {@link OAuth2ParameterNames#CODE} parameter.
|
||||
*/
|
||||
String CODE = OAuth2Authorization.class.getName().concat(".CODE");
|
||||
|
||||
/**
|
||||
* The name of the attribute used for the {@link OAuth2AuthorizationRequest}.
|
||||
*/
|
||||
String AUTHORIZATION_REQUEST = OAuth2Authorization.class.getName().concat(".AUTHORIZATION_REQUEST");
|
||||
|
||||
/**
|
||||
* The name of the attribute used for the attributes/claims of the {@link OAuth2AccessToken}.
|
||||
*/
|
||||
String ACCESS_TOKEN_ATTRIBUTES = OAuth2Authorization.class.getName().concat(".ACCESS_TOKEN_ATTRIBUTES");
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AbstractOAuth2Token}
|
||||
* representing an OAuth 2.0 Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.3
|
||||
* @see AbstractOAuth2Token
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
|
||||
*/
|
||||
public class OAuth2AuthorizationCode extends AbstractOAuth2Token {
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCode} using the provided parameters.
|
||||
* @param tokenValue the token value
|
||||
* @param issuedAt the time at which the token was issued
|
||||
* @param expiresAt the time at which the token expires
|
||||
*/
|
||||
public OAuth2AuthorizationCode(String tokenValue, Instant issuedAt, Instant expiresAt) {
|
||||
super(tokenValue, issuedAt, expiresAt);
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
|
@ -16,6 +16,7 @@
|
|||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for the management
|
||||
|
@ -24,6 +25,7 @@ import org.springframework.lang.Nullable;
|
|||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see OAuth2Authorization
|
||||
* @see OAuth2TokenType
|
||||
*/
|
||||
public interface OAuth2AuthorizationService {
|
||||
|
||||
|
@ -34,14 +36,32 @@ public interface OAuth2AuthorizationService {
|
|||
*/
|
||||
void save(OAuth2Authorization authorization);
|
||||
|
||||
/**
|
||||
* Removes the {@link OAuth2Authorization}.
|
||||
*
|
||||
* @param authorization the {@link OAuth2Authorization}
|
||||
*/
|
||||
void remove(OAuth2Authorization authorization);
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2Authorization} identified by the provided {@code id},
|
||||
* or {@code null} if not found.
|
||||
*
|
||||
* @param id the authorization identifier
|
||||
* @return the {@link OAuth2Authorization} if found, otherwise {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
OAuth2Authorization findById(String id);
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2Authorization} containing the provided {@code token},
|
||||
* or {@code null} if not found.
|
||||
*
|
||||
* @param token the token credential
|
||||
* @param tokenType the {@link TokenType token type}
|
||||
* @param tokenType the {@link OAuth2TokenType token type}
|
||||
* @return the {@link OAuth2Authorization} if found, otherwise {@code null}
|
||||
*/
|
||||
OAuth2Authorization findByToken(String token, @Nullable TokenType tokenType);
|
||||
@Nullable
|
||||
OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see Context
|
||||
*/
|
||||
public interface OAuth2TokenContext extends Context {
|
||||
|
||||
default RegisteredClient getRegisteredClient() {
|
||||
return get(RegisteredClient.class);
|
||||
}
|
||||
|
||||
default <T extends Authentication> T getPrincipal() {
|
||||
return get(AbstractBuilder.PRINCIPAL_AUTHENTICATION_KEY);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
default OAuth2Authorization getAuthorization() {
|
||||
return get(OAuth2Authorization.class);
|
||||
}
|
||||
|
||||
default Set<String> getAuthorizedScopes() {
|
||||
return hasKey(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME) ?
|
||||
get(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME) :
|
||||
Collections.emptySet();
|
||||
}
|
||||
|
||||
default OAuth2TokenType getTokenType() {
|
||||
return get(OAuth2TokenType.class);
|
||||
}
|
||||
|
||||
default AuthorizationGrantType getAuthorizationGrantType() {
|
||||
return get(AuthorizationGrantType.class);
|
||||
}
|
||||
|
||||
default <T extends Authentication> T getAuthorizationGrant() {
|
||||
return get(AbstractBuilder.AUTHORIZATION_GRANT_AUTHENTICATION_KEY);
|
||||
}
|
||||
|
||||
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<>();
|
||||
|
||||
public B registeredClient(RegisteredClient registeredClient) {
|
||||
return put(RegisteredClient.class, registeredClient);
|
||||
}
|
||||
|
||||
public B principal(Authentication principal) {
|
||||
return put(PRINCIPAL_AUTHENTICATION_KEY, principal);
|
||||
}
|
||||
|
||||
public B authorization(OAuth2Authorization authorization) {
|
||||
return put(OAuth2Authorization.class, authorization);
|
||||
}
|
||||
|
||||
public B authorizedScopes(Set<String> authorizedScopes) {
|
||||
return put(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
|
||||
}
|
||||
|
||||
public B tokenType(OAuth2TokenType tokenType) {
|
||||
return put(OAuth2TokenType.class, tokenType);
|
||||
}
|
||||
|
||||
public B authorizationGrantType(AuthorizationGrantType authorizationGrantType) {
|
||||
return put(AuthorizationGrantType.class, authorizationGrantType);
|
||||
}
|
||||
|
||||
public B authorizationGrant(Authentication authorizationGrant) {
|
||||
return put(AUTHORIZATION_GRANT_AUTHENTICATION_KEY, authorizationGrant);
|
||||
}
|
||||
|
||||
public B put(Object key, Object value) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
this.context.put(key, value);
|
||||
return getThis();
|
||||
}
|
||||
|
||||
public B context(Consumer<Map<Object, Object>> contextConsumer) {
|
||||
contextConsumer.accept(this.context);
|
||||
return getThis();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <V> V get(Object key) {
|
||||
return (V) this.context.get(key);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected B getThis() {
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
public abstract T build();
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see OAuth2TokenContext
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface OAuth2TokenCustomizer<C extends OAuth2TokenContext> {
|
||||
|
||||
void customize(C context);
|
||||
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -15,32 +15,38 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion2;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
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 when issuing an OAuth 2.0 Access Token.
|
||||
* An {@link Authentication} implementation used when issuing an
|
||||
* OAuth 2.0 Access Token and (optional) Refresh Token.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Madhu Bhat
|
||||
* @since 0.0.1
|
||||
* @see AbstractAuthenticationToken
|
||||
* @see OAuth2AuthorizationCodeAuthenticationProvider
|
||||
* @see RegisteredClient
|
||||
* @see OAuth2AccessToken
|
||||
* @see OAuth2RefreshToken
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
*/
|
||||
public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private final RegisteredClient registeredClient;
|
||||
private final Authentication clientPrincipal;
|
||||
private final OAuth2AccessToken accessToken;
|
||||
private final OAuth2RefreshToken refreshToken;
|
||||
private final Map<String, Object> additionalParameters;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided parameters.
|
||||
|
@ -51,13 +57,43 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication
|
|||
*/
|
||||
public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient,
|
||||
Authentication clientPrincipal, OAuth2AccessToken accessToken) {
|
||||
this(registeredClient, clientPrincipal, accessToken, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param registeredClient the registered client
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
* @param accessToken the access token
|
||||
* @param refreshToken the refresh token
|
||||
*/
|
||||
public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, Authentication clientPrincipal,
|
||||
OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) {
|
||||
this(registeredClient, clientPrincipal, accessToken, refreshToken, Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param registeredClient the registered client
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
* @param accessToken the access token
|
||||
* @param refreshToken the refresh token
|
||||
* @param additionalParameters the additional parameters
|
||||
*/
|
||||
public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, Authentication clientPrincipal,
|
||||
OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken, Map<String, Object> additionalParameters) {
|
||||
super(Collections.emptyList());
|
||||
Assert.notNull(registeredClient, "registeredClient cannot be null");
|
||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
||||
Assert.notNull(accessToken, "accessToken cannot be null");
|
||||
Assert.notNull(additionalParameters, "additionalParameters cannot be null");
|
||||
this.registeredClient = registeredClient;
|
||||
this.clientPrincipal = clientPrincipal;
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
this.additionalParameters = additionalParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -87,4 +123,23 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication
|
|||
public OAuth2AccessToken getAccessToken() {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2RefreshToken refresh token}.
|
||||
*
|
||||
* @return the {@link OAuth2RefreshToken} or {@code null} if not available
|
||||
*/
|
||||
@Nullable
|
||||
public OAuth2RefreshToken getRefreshToken() {
|
||||
return this.refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional parameters.
|
||||
*
|
||||
* @return a {@code Map} of the additional parameters, may be empty
|
||||
*/
|
||||
public Map<String, Object> getAdditionalParameters() {
|
||||
return this.additionalParameters;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.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.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.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.3
|
||||
*/
|
||||
final class OAuth2AuthenticationProviderUtils {
|
||||
|
||||
private OAuth2AuthenticationProviderUtils() {
|
||||
}
|
||||
|
||||
static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||
}
|
||||
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||
return clientPrincipal;
|
||||
}
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
}
|
||||
|
||||
static <T extends AbstractOAuth2Token> OAuth2Authorization invalidate(
|
||||
OAuth2Authorization authorization, T token) {
|
||||
|
||||
// @formatter:off
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization)
|
||||
.token(token,
|
||||
(metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
|
||||
|
||||
if (OAuth2RefreshToken.class.isAssignableFrom(token.getClass())) {
|
||||
authorizationBuilder.token(
|
||||
authorization.getAccessToken().getToken(),
|
||||
(metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
|
||||
authorization.getToken(OAuth2AuthorizationCode.class);
|
||||
if (authorizationCode != null && !authorizationCode.isInvalidated()) {
|
||||
authorizationBuilder.token(
|
||||
authorizationCode.getToken(),
|
||||
(metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
|
||||
}
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
return authorizationBuilder.build();
|
||||
}
|
||||
}
|
|
@ -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,145 +15,238 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
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.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.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.JoseHeader;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
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.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
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.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.0.1
|
||||
* @see OAuth2AuthorizationCodeAuthenticationToken
|
||||
* @see OAuth2AccessTokenAuthenticationToken
|
||||
* @see RegisteredClientRepository
|
||||
* @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>
|
||||
*/
|
||||
public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param registeredClientRepository the repository of registered clients
|
||||
* @param authorizationService the authorization service
|
||||
* @param jwtEncoder the jwt encoder
|
||||
*/
|
||||
public OAuth2AuthorizationCodeAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
|
||||
OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
public OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.authorizationService = authorizationService;
|
||||
this.jwtEncoder = jwtEncoder;
|
||||
}
|
||||
|
||||
public final void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
|
||||
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
|
||||
this.jwtCustomizer = jwtCustomizer;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
protected void setProviderSettings(ProviderSettings providerSettings) {
|
||||
this.providerSettings = providerSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
|
||||
(OAuth2AuthorizationCodeAuthenticationToken) authentication;
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authorizationCodeAuthentication.getPrincipal().getClass())) {
|
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) authorizationCodeAuthentication.getPrincipal();
|
||||
}
|
||||
if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
}
|
||||
|
||||
// TODO Authenticate public client
|
||||
// A client MAY use the "client_id" request parameter to identify itself
|
||||
// when sending requests to the token endpoint.
|
||||
// In the "authorization_code" "grant_type" request to the token endpoint,
|
||||
// an unauthenticated client MUST send its "client_id" to prevent itself
|
||||
// from inadvertently accepting a code intended for a client with a different "client_id".
|
||||
// This protects the client from substitution of the authentication code.
|
||||
OAuth2ClientAuthenticationToken clientPrincipal =
|
||||
getAuthenticatedClientElseThrowInvalidClient(authorizationCodeAuthentication);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
authorizationCodeAuthentication.getCode(), TokenType.AUTHORIZATION_CODE);
|
||||
authorizationCodeAuthentication.getCode(), AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
}
|
||||
if (!clientPrincipal.getRegisteredClient().getId().equals(authorization.getRegisteredClientId())) {
|
||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
|
||||
authorization.getToken(OAuth2AuthorizationCode.class);
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
||||
OAuth2AuthorizationRequest.class.getName());
|
||||
|
||||
if (!registeredClient.getClientId().equals(authorizationRequest.getClientId())) {
|
||||
if (!authorizationCode.isInvalidated()) {
|
||||
// Invalidate the authorization code given that a different client is attempting to use it
|
||||
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken());
|
||||
this.authorizationService.save(authorization);
|
||||
}
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
||||
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
|
||||
if (StringUtils.hasText(authorizationRequest.getRedirectUri()) &&
|
||||
!authorizationRequest.getRedirectUri().equals(authorizationCodeAuthentication.getRedirectUri())) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
}
|
||||
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
if (authorizationCode.isInvalidated()) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
}
|
||||
|
||||
// TODO Allow configuration for issuer claim
|
||||
URL issuer = null;
|
||||
try {
|
||||
issuer = URI.create("https://oauth2.provider.com").toURL();
|
||||
} catch (MalformedURLException e) { }
|
||||
String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null;
|
||||
Set<String> authorizedScopes = authorization.getAttribute(
|
||||
OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); // TODO Allow configuration for access token time-to-live
|
||||
JoseHeader.Builder headersBuilder = JwtUtils.headers();
|
||||
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(
|
||||
registeredClient, issuer, authorization.getPrincipalName(),
|
||||
authorizedScopes);
|
||||
|
||||
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.withClaims()
|
||||
.issuer(issuer)
|
||||
.subject(authorization.getPrincipalName())
|
||||
.audience(Collections.singletonList(clientPrincipal.getRegisteredClient().getClientId()))
|
||||
.issuedAt(issuedAt)
|
||||
.expiresAt(expiresAt)
|
||||
.notBefore(issuedAt)
|
||||
.claim(OAuth2ParameterNames.SCOPE, authorizationRequest.getScopes())
|
||||
// @formatter:off
|
||||
JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
|
||||
.registeredClient(registeredClient)
|
||||
.principal(authorization.getAttribute(Principal.class.getName()))
|
||||
.authorization(authorization)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrant(authorizationCodeAuthentication)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
Jwt jwt = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
this.jwtCustomizer.customize(context);
|
||||
|
||||
JoseHeader headers = context.getHeaders().build();
|
||||
JwtClaimsSet claims = context.getClaims().build();
|
||||
Jwt jwtAccessToken = this.jwtEncoder.encode(headers, claims);
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
|
||||
jwtAccessToken.getExpiresAt(), authorizedScopes);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
OidcIdToken idToken;
|
||||
if (jwtIdToken != null) {
|
||||
idToken = new OidcIdToken(jwtIdToken.getTokenValue(), jwtIdToken.getIssuedAt(),
|
||||
jwtIdToken.getExpiresAt(), jwtIdToken.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());
|
||||
|
||||
authorization = OAuth2Authorization.from(authorization)
|
||||
.attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, jwt)
|
||||
.accessToken(accessToken)
|
||||
.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(
|
||||
clientPrincipal.getRegisteredClient(), clientPrincipal, accessToken);
|
||||
registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,30 +15,26 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion2;
|
||||
import org.springframework.util.Assert;
|
||||
import java.util.Map;
|
||||
|
||||
import java.util.Collections;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation used for the OAuth 2.0 Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Madhu Bhat
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.0.1
|
||||
* @see AbstractAuthenticationToken
|
||||
* @see OAuth2AuthorizationGrantAuthenticationToken
|
||||
* @see OAuth2AuthorizationCodeAuthenticationProvider
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
*/
|
||||
public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
private String code;
|
||||
private Authentication clientPrincipal;
|
||||
private String clientId;
|
||||
private String redirectUri;
|
||||
public class OAuth2AuthorizationCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
private final String code;
|
||||
private final String redirectUri;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationToken} using the provided parameters.
|
||||
|
@ -46,44 +42,16 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
|
|||
* @param code the authorization code
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
* @param redirectUri the redirect uri
|
||||
* @param additionalParameters the additional parameters
|
||||
*/
|
||||
public OAuth2AuthorizationCodeAuthenticationToken(String code,
|
||||
Authentication clientPrincipal, @Nullable String redirectUri) {
|
||||
super(Collections.emptyList());
|
||||
public OAuth2AuthorizationCodeAuthenticationToken(String code, Authentication clientPrincipal,
|
||||
@Nullable String redirectUri, @Nullable Map<String, Object> additionalParameters) {
|
||||
super(AuthorizationGrantType.AUTHORIZATION_CODE, clientPrincipal, additionalParameters);
|
||||
Assert.hasText(code, "code cannot be empty");
|
||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
||||
this.code = code;
|
||||
this.clientPrincipal = clientPrincipal;
|
||||
this.redirectUri = redirectUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param code the authorization code
|
||||
* @param clientId the client identifier
|
||||
* @param redirectUri the redirect uri
|
||||
*/
|
||||
public OAuth2AuthorizationCodeAuthenticationToken(String code,
|
||||
String clientId, @Nullable String redirectUri) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(code, "code cannot be empty");
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
this.code = code;
|
||||
this.clientId = clientId;
|
||||
this.redirectUri = redirectUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.clientPrincipal != null ? this.clientPrincipal : this.clientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization code.
|
||||
*
|
||||
|
@ -98,7 +66,8 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
|
|||
*
|
||||
* @return the redirect uri
|
||||
*/
|
||||
public @Nullable String getRedirectUri() {
|
||||
@Nullable
|
||||
public String getRedirectUri() {
|
||||
return this.redirectUri;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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.Collections;
|
||||
import java.util.HashMap;
|
||||
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.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Base implementation of an {@link Authentication} representing an OAuth 2.0 Authorization Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see AbstractAuthenticationToken
|
||||
* @see AuthorizationGrantType
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
|
||||
*/
|
||||
public class OAuth2AuthorizationGrantAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private final AuthorizationGrantType authorizationGrantType;
|
||||
private final Authentication clientPrincipal;
|
||||
private final Map<String, Object> additionalParameters;
|
||||
|
||||
/**
|
||||
* Sub-class constructor.
|
||||
*
|
||||
* @param authorizationGrantType the authorization grant type
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
* @param additionalParameters the additional parameters
|
||||
*/
|
||||
protected OAuth2AuthorizationGrantAuthenticationToken(AuthorizationGrantType authorizationGrantType,
|
||||
Authentication clientPrincipal, @Nullable Map<String, Object> additionalParameters) {
|
||||
super(Collections.emptyList());
|
||||
Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
|
||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
||||
this.authorizationGrantType = authorizationGrantType;
|
||||
this.clientPrincipal = clientPrincipal;
|
||||
this.additionalParameters = Collections.unmodifiableMap(
|
||||
additionalParameters != null ?
|
||||
new HashMap<>(additionalParameters) :
|
||||
Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization grant type.
|
||||
*
|
||||
* @return the authorization grant type
|
||||
*/
|
||||
public AuthorizationGrantType getGrantType() {
|
||||
return this.authorizationGrantType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.clientPrincipal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional parameters.
|
||||
*
|
||||
* @return the additional parameters
|
||||
*/
|
||||
public Map<String, Object> getAdditionalParameters() {
|
||||
return this.additionalParameters;
|
||||
}
|
||||
}
|
|
@ -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,50 +15,93 @@
|
|||
*/
|
||||
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.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.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.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation that validates {@link OAuth2ClientAuthenticationToken}'s.
|
||||
* An {@link AuthenticationProvider} implementation used for authenticating an OAuth 2.0 Client.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Patryk Kostrzewa
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.0.1
|
||||
* @see AuthenticationProvider
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param registeredClientRepository the repository of registered clients
|
||||
* @param authorizationService the authorization service
|
||||
*/
|
||||
public OAuth2ClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository) {
|
||||
public OAuth2ClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
|
||||
OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String clientId = authentication.getPrincipal().toString();
|
||||
OAuth2ClientAuthenticationToken clientAuthentication =
|
||||
(OAuth2ClientAuthenticationToken) authentication;
|
||||
|
||||
String clientId = clientAuthentication.getPrincipal().toString();
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
|
||||
if (registeredClient == null) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
throwInvalidClient();
|
||||
}
|
||||
|
||||
String clientSecret = authentication.getCredentials().toString();
|
||||
if (!registeredClient.getClientSecret().equals(clientSecret)) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
if (!registeredClient.getClientAuthenticationMethods().contains(
|
||||
clientAuthentication.getClientAuthenticationMethod())) {
|
||||
throwInvalidClient();
|
||||
}
|
||||
|
||||
boolean authenticatedCredentials = false;
|
||||
|
||||
if (clientAuthentication.getCredentials() != null) {
|
||||
String clientSecret = clientAuthentication.getCredentials().toString();
|
||||
// TODO Use PasswordEncoder.matches()
|
||||
if (!registeredClient.getClientSecret().equals(clientSecret)) {
|
||||
throwInvalidClient();
|
||||
}
|
||||
authenticatedCredentials = true;
|
||||
}
|
||||
|
||||
authenticatedCredentials = authenticatedCredentials ||
|
||||
authenticatePkceIfAvailable(clientAuthentication, registeredClient);
|
||||
if (!authenticatedCredentials) {
|
||||
throwInvalidClient();
|
||||
}
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(registeredClient);
|
||||
|
@ -68,4 +111,68 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
|
|||
public boolean supports(Class<?> authentication) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,26 +18,31 @@ package org.springframework.security.oauth2.server.authorization.authentication;
|
|||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion2;
|
||||
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.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Patryk Kostrzewa
|
||||
* @author Anoop Garlapati
|
||||
* @since 0.0.1
|
||||
* @see AbstractAuthenticationToken
|
||||
* @see RegisteredClient
|
||||
* @see OAuth2ClientAuthenticationProvider
|
||||
*/
|
||||
public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -45,13 +50,33 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
|
|||
*
|
||||
* @param clientId the client identifier
|
||||
* @param clientSecret the client secret
|
||||
* @param clientAuthenticationMethod the authentication method used by the client
|
||||
* @param additionalParameters the additional parameters
|
||||
*/
|
||||
public OAuth2ClientAuthenticationToken(String clientId, String clientSecret) {
|
||||
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) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.hasText(clientSecret, "clientSecret cannot be empty");
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
this.additionalParameters = additionalParameters != null ?
|
||||
Collections.unmodifiableMap(additionalParameters) : null;
|
||||
this.clientAuthenticationMethod = ClientAuthenticationMethod.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -78,6 +103,15 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
|
|||
return this.clientSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional parameters
|
||||
*
|
||||
* @return the additional parameters
|
||||
*/
|
||||
public @Nullable Map<String, Object> getAdditionalParameters() {
|
||||
return this.additionalParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link RegisteredClient registered client}.
|
||||
*
|
||||
|
@ -86,4 +120,13 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
|
|||
public @Nullable RegisteredClient getRegisteredClient() {
|
||||
return this.registeredClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ClientAuthenticationMethod client authentication method}.
|
||||
*
|
||||
* @return the {@link ClientAuthenticationMethod}
|
||||
*/
|
||||
public @Nullable ClientAuthenticationMethod getClientAuthenticationMethod() {
|
||||
return this.clientAuthenticationMethod;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,51 +15,55 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.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.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.JoseHeader;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
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.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
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.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Client Credentials Grant.
|
||||
*
|
||||
* @author Alexey Nesterov
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @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>
|
||||
*/
|
||||
public class OAuth2ClientCredentialsAuthenticationProvider implements AuthenticationProvider {
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final JwtEncoder jwtEncoder;
|
||||
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
|
||||
private ProviderSettings providerSettings;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the provided parameters.
|
||||
|
@ -75,21 +79,30 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica
|
|||
this.jwtEncoder = jwtEncoder;
|
||||
}
|
||||
|
||||
public final void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
|
||||
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
|
||||
this.jwtCustomizer = jwtCustomizer;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
protected void setProviderSettings(ProviderSettings providerSettings) {
|
||||
this.providerSettings = providerSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication =
|
||||
(OAuth2ClientCredentialsAuthenticationToken) authentication;
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(clientCredentialsAuthentication.getPrincipal().getClass())) {
|
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) clientCredentialsAuthentication.getPrincipal();
|
||||
}
|
||||
if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
}
|
||||
OAuth2ClientAuthenticationToken clientPrincipal =
|
||||
getAuthenticatedClientElseThrowInvalidClient(clientCredentialsAuthentication);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
Set<String> scopes = registeredClient.getScopes(); // Default to configured scopes
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.CLIENT_CREDENTIALS)) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(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))
|
||||
|
@ -97,40 +110,47 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica
|
|||
if (!CollectionUtils.isEmpty(unauthorizedScopes)) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE));
|
||||
}
|
||||
scopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
|
||||
authorizedScopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
|
||||
}
|
||||
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null;
|
||||
|
||||
// TODO Allow configuration for issuer claim
|
||||
URL issuer = null;
|
||||
try {
|
||||
issuer = URI.create("https://oauth2.provider.com").toURL();
|
||||
} catch (MalformedURLException e) { }
|
||||
JoseHeader.Builder headersBuilder = JwtUtils.headers();
|
||||
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(
|
||||
registeredClient, issuer, clientPrincipal.getName(), authorizedScopes);
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); // TODO Allow configuration for access token time-to-live
|
||||
|
||||
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.withClaims()
|
||||
.issuer(issuer)
|
||||
.subject(clientPrincipal.getName())
|
||||
.audience(Collections.singletonList(registeredClient.getClientId()))
|
||||
.issuedAt(issuedAt)
|
||||
.expiresAt(expiresAt)
|
||||
.notBefore(issuedAt)
|
||||
.claim(OAuth2ParameterNames.SCOPE, scopes)
|
||||
// @formatter:off
|
||||
JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
|
||||
.registeredClient(registeredClient)
|
||||
.principal(clientPrincipal)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.authorizationGrant(clientCredentialsAuthentication)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
Jwt jwt = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
this.jwtCustomizer.customize(context);
|
||||
|
||||
JoseHeader headers = context.getHeaders().build();
|
||||
JwtClaimsSet claims = context.getClaims().build();
|
||||
Jwt jwtAccessToken = this.jwtEncoder.encode(headers, claims);
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), scopes);
|
||||
jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
|
||||
jwtAccessToken.getExpiresAt(), authorizedScopes);
|
||||
|
||||
// @formatter:off
|
||||
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, jwt)
|
||||
.principalName(clientPrincipal.getName())
|
||||
.accessToken(accessToken)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.token(accessToken,
|
||||
(metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, jwtAccessToken.getClaims()))
|
||||
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
|
||||
|
|
|
@ -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,60 +15,38 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion2;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation used for the OAuth 2.0 Client Credentials Grant.
|
||||
*
|
||||
* @author Alexey Nesterov
|
||||
* @since 0.0.1
|
||||
* @see AbstractAuthenticationToken
|
||||
* @see OAuth2AuthorizationGrantAuthenticationToken
|
||||
* @see OAuth2ClientCredentialsAuthenticationProvider
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
*/
|
||||
public class OAuth2ClientCredentialsAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
private final Authentication clientPrincipal;
|
||||
public class OAuth2ClientCredentialsAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
private final Set<String> scopes;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientCredentialsAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
*/
|
||||
public OAuth2ClientCredentialsAuthenticationToken(Authentication clientPrincipal) {
|
||||
this(clientPrincipal, Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientCredentialsAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
* @param scopes the requested scope(s)
|
||||
* @param additionalParameters the additional parameters
|
||||
*/
|
||||
public OAuth2ClientCredentialsAuthenticationToken(Authentication clientPrincipal, Set<String> scopes) {
|
||||
super(Collections.emptyList());
|
||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
||||
Assert.notNull(scopes, "scopes cannot be null");
|
||||
this.clientPrincipal = clientPrincipal;
|
||||
this.scopes = Collections.unmodifiableSet(new LinkedHashSet<>(scopes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.clientPrincipal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
public OAuth2ClientCredentialsAuthenticationToken(Authentication clientPrincipal,
|
||||
@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
|
||||
super(AuthorizationGrantType.CLIENT_CREDENTIALS, clientPrincipal, additionalParameters);
|
||||
this.scopes = Collections.unmodifiableSet(
|
||||
scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Set;
|
||||
|
||||
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.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.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.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.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.util.Assert;
|
||||
|
||||
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Refresh Token Grant.
|
||||
*
|
||||
* @author Alexey Nesterov
|
||||
* @author Joe Grandja
|
||||
* @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>
|
||||
*/
|
||||
public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final StringKeyGenerator TOKEN_GENERATOR = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final JwtEncoder jwtEncoder;
|
||||
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
|
||||
private ProviderSettings providerSettings;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param authorizationService the authorization service
|
||||
* @param jwtEncoder the jwt encoder
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
public final void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
|
||||
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
|
||||
this.jwtCustomizer = jwtCustomizer;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
protected void setProviderSettings(ProviderSettings providerSettings) {
|
||||
this.providerSettings = providerSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2RefreshTokenAuthenticationToken refreshTokenAuthentication =
|
||||
(OAuth2RefreshTokenAuthenticationToken) authentication;
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal =
|
||||
getAuthenticatedClientElseThrowInvalidClient(refreshTokenAuthentication);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
refreshTokenAuthentication.getRefreshToken(), OAuth2TokenType.REFRESH_TOKEN);
|
||||
if (authorization == null) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
}
|
||||
|
||||
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
}
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT));
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = authorization.getRefreshToken();
|
||||
Instant refreshTokenExpiresAt = refreshToken.getToken().getExpiresAt();
|
||||
if (refreshTokenExpiresAt.isBefore(Instant.now())) {
|
||||
// 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));
|
||||
}
|
||||
|
||||
// As per https://tools.ietf.org/html/rfc6749#section-6
|
||||
// The requested scope MUST NOT include any scope not originally granted by the resource owner,
|
||||
// and if omitted is treated as equal to the scope originally granted by the resource owner.
|
||||
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));
|
||||
}
|
||||
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)
|
||||
.registeredClient(registeredClient)
|
||||
.principal(authorization.getAttribute(Principal.class.getName()))
|
||||
.authorization(authorization)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||
.authorizationGrant(refreshTokenAuthentication)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
this.jwtCustomizer.customize(context);
|
||||
|
||||
JoseHeader headers = context.getHeaders().build();
|
||||
JwtClaimsSet claims = context.getClaims().build();
|
||||
Jwt jwtAccessToken = this.jwtEncoder.encode(headers, claims);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
authorization = OAuth2Authorization.from(authorization)
|
||||
.token(accessToken,
|
||||
(metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, jwtAccessToken.getClaims()))
|
||||
.refreshToken(currentRefreshToken)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
return new OAuth2AccessTokenAuthenticationToken(
|
||||
registeredClient, clientPrincipal, accessToken, currentRefreshToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation used for the OAuth 2.0 Refresh Token Grant.
|
||||
*
|
||||
* @author Alexey Nesterov
|
||||
* @since 0.0.3
|
||||
* @see OAuth2AuthorizationGrantAuthenticationToken
|
||||
* @see OAuth2RefreshTokenAuthenticationProvider
|
||||
*/
|
||||
public class OAuth2RefreshTokenAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
private final String refreshToken;
|
||||
private final Set<String> scopes;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2RefreshTokenAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param refreshToken the refresh token
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
* @param scopes the requested scope(s)
|
||||
* @param additionalParameters the additional parameters
|
||||
*/
|
||||
public OAuth2RefreshTokenAuthenticationToken(String refreshToken, Authentication clientPrincipal,
|
||||
@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
|
||||
super(AuthorizationGrantType.REFRESH_TOKEN, clientPrincipal, additionalParameters);
|
||||
Assert.hasText(refreshToken, "refreshToken cannot be empty");
|
||||
this.refreshToken = refreshToken;
|
||||
this.scopes = Collections.unmodifiableSet(
|
||||
scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the refresh token.
|
||||
*
|
||||
* @return the refresh token
|
||||
*/
|
||||
public String getRefreshToken() {
|
||||
return this.refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested scope(s).
|
||||
*
|
||||
* @return the requested scope(s), or an empty {@code Set} if not available
|
||||
*/
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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.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.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;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for OAuth 2.0 Token Revocation.
|
||||
*
|
||||
* @author Vivek Babu
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.3
|
||||
* @see OAuth2TokenRevocationAuthenticationToken
|
||||
* @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 {
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2TokenRevocationAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param authorizationService the authorization service
|
||||
*/
|
||||
public OAuth2TokenRevocationAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2TokenRevocationAuthenticationToken tokenRevocationAuthentication =
|
||||
(OAuth2TokenRevocationAuthenticationToken) authentication;
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal =
|
||||
getAuthenticatedClientElseThrowInvalidClient(tokenRevocationAuthentication);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
tokenRevocationAuthentication.getToken(), null);
|
||||
if (authorization == null) {
|
||||
// Return the authentication request when token not found
|
||||
return tokenRevocationAuthentication;
|
||||
}
|
||||
|
||||
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<AbstractOAuth2Token> token = authorization.getToken(tokenRevocationAuthentication.getToken());
|
||||
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, token.getToken());
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
return new OAuth2TokenRevocationAuthenticationToken(token.getToken(), clientPrincipal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2TokenRevocationAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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.authentication;
|
||||
|
||||
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.Version;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation used for OAuth 2.0 Token Revocation.
|
||||
*
|
||||
* @author Vivek Babu
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.3
|
||||
* @see AbstractAuthenticationToken
|
||||
* @see OAuth2TokenRevocationAuthenticationProvider
|
||||
*/
|
||||
public class OAuth2TokenRevocationAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private final String token;
|
||||
private final Authentication clientPrincipal;
|
||||
private final String tokenTypeHint;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2TokenRevocationAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param token the token
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
* @param tokenTypeHint the token type hint
|
||||
*/
|
||||
public OAuth2TokenRevocationAuthenticationToken(String token,
|
||||
Authentication clientPrincipal, @Nullable String tokenTypeHint) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(token, "token cannot be empty");
|
||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
||||
this.token = token;
|
||||
this.clientPrincipal = clientPrincipal;
|
||||
this.tokenTypeHint = tokenTypeHint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2TokenRevocationAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param revokedToken the revoked token
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
*/
|
||||
public OAuth2TokenRevocationAuthenticationToken(AbstractOAuth2Token revokedToken,
|
||||
Authentication clientPrincipal) {
|
||||
super(Collections.emptyList());
|
||||
Assert.notNull(revokedToken, "revokedToken cannot be null");
|
||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
||||
this.token = revokedToken.getTokenValue();
|
||||
this.clientPrincipal = clientPrincipal;
|
||||
this.tokenTypeHint = null;
|
||||
setAuthenticated(true); // Indicates that the token was authenticated and revoked
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.clientPrincipal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the token.
|
||||
*
|
||||
* @return the token
|
||||
*/
|
||||
public String getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the token type hint.
|
||||
*
|
||||
* @return the token type hint
|
||||
*/
|
||||
@Nullable
|
||||
public String getTokenTypeHint() {
|
||||
return this.tokenTypeHint;
|
||||
}
|
||||
}
|
|
@ -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,16 +15,20 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.client;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link RegisteredClientRepository} that stores {@link RegisteredClient}(s) in-memory.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation is recommended ONLY to be used during development/testing.
|
||||
*
|
||||
* @author Anoop Garlapati
|
||||
* @see RegisteredClientRepository
|
||||
* @see RegisteredClient
|
||||
|
@ -71,12 +75,14 @@ public final class InMemoryRegisteredClientRepository implements RegisteredClien
|
|||
this.clientIdRegistrationMap = clientIdRegistrationMapResult;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RegisteredClient findById(String id) {
|
||||
Assert.hasText(id, "id cannot be empty");
|
||||
return this.idRegistrationMap.get(id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RegisteredClient findByClientId(String clientId) {
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
|
|
|
@ -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,20 +15,23 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.client;
|
||||
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion2;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A representation of a client registration with an OAuth 2.0 Authorization Server.
|
||||
*
|
||||
|
@ -38,7 +41,7 @@ import java.util.function.Consumer;
|
|||
* @since 0.0.1
|
||||
*/
|
||||
public class RegisteredClient implements Serializable {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private String id;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
|
@ -46,6 +49,8 @@ public class RegisteredClient implements Serializable {
|
|||
private Set<AuthorizationGrantType> authorizationGrantTypes;
|
||||
private Set<String> redirectUris;
|
||||
private Set<String> scopes;
|
||||
private ClientSettings clientSettings;
|
||||
private TokenSettings tokenSettings;
|
||||
|
||||
protected RegisteredClient() {
|
||||
}
|
||||
|
@ -78,8 +83,7 @@ public class RegisteredClient implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ClientAuthenticationMethod authentication method(s)} used
|
||||
* when authenticating the client with the authorization server.
|
||||
* Returns the {@link ClientAuthenticationMethod authentication method(s)} that the client may use.
|
||||
*
|
||||
* @return the {@code Set} of {@link ClientAuthenticationMethod authentication method(s)}
|
||||
*/
|
||||
|
@ -106,7 +110,7 @@ public class RegisteredClient implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the scope(s) used by the client.
|
||||
* Returns the scope(s) that the client may use.
|
||||
*
|
||||
* @return the {@code Set} of scope(s)
|
||||
*/
|
||||
|
@ -114,15 +118,62 @@ public class RegisteredClient implements Serializable {
|
|||
return this.scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ClientSettings client configuration settings}.
|
||||
*
|
||||
* @return the {@link ClientSettings}
|
||||
*/
|
||||
public ClientSettings getClientSettings() {
|
||||
return this.clientSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link TokenSettings token configuration settings}.
|
||||
*
|
||||
* @return the {@link TokenSettings}
|
||||
*/
|
||||
public TokenSettings getTokenSettings() {
|
||||
return this.tokenSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
RegisteredClient that = (RegisteredClient) obj;
|
||||
return Objects.equals(this.id, that.id) &&
|
||||
Objects.equals(this.clientId, that.clientId) &&
|
||||
Objects.equals(this.clientSecret, that.clientSecret) &&
|
||||
Objects.equals(this.clientAuthenticationMethods, that.clientAuthenticationMethods) &&
|
||||
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());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.id, this.clientId, this.clientSecret,
|
||||
this.clientAuthenticationMethods, this.authorizationGrantTypes, this.redirectUris,
|
||||
this.scopes, this.clientSettings.settings(), this.tokenSettings.settings());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RegisteredClient{" +
|
||||
return "RegisteredClient {" +
|
||||
"id='" + this.id + '\'' +
|
||||
", clientId='" + this.clientId + '\'' +
|
||||
", clientAuthenticationMethods=" + this.clientAuthenticationMethods +
|
||||
", authorizationGrantTypes=" + this.authorizationGrantTypes +
|
||||
", redirectUris=" + this.redirectUris +
|
||||
", scopes=" + this.scopes +
|
||||
", clientSettings=" + this.clientSettings.settings() +
|
||||
", tokenSettings=" + this.tokenSettings.settings() +
|
||||
'}';
|
||||
}
|
||||
|
||||
|
@ -138,12 +189,12 @@ public class RegisteredClient implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@link RegisteredClient}.
|
||||
* Returns a new {@link Builder}, initialized with the values from the provided {@link RegisteredClient}.
|
||||
*
|
||||
* @param registeredClient the {@link RegisteredClient} to copy from
|
||||
* @param registeredClient the {@link RegisteredClient} used for initializing the {@link Builder}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder withRegisteredClient(RegisteredClient registeredClient) {
|
||||
public static Builder from(RegisteredClient registeredClient) {
|
||||
Assert.notNull(registeredClient, "registeredClient cannot be null");
|
||||
return new Builder(registeredClient);
|
||||
}
|
||||
|
@ -152,14 +203,16 @@ public class RegisteredClient implements Serializable {
|
|||
* A builder for {@link RegisteredClient}.
|
||||
*/
|
||||
public static class Builder implements Serializable {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private String id;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private Set<ClientAuthenticationMethod> clientAuthenticationMethods = new LinkedHashSet<>();
|
||||
private Set<AuthorizationGrantType> authorizationGrantTypes = new LinkedHashSet<>();
|
||||
private Set<String> redirectUris = new LinkedHashSet<>();
|
||||
private Set<String> scopes = new LinkedHashSet<>();
|
||||
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();
|
||||
|
||||
protected Builder(String id) {
|
||||
this.id = id;
|
||||
|
@ -181,6 +234,8 @@ public class RegisteredClient implements Serializable {
|
|||
if (!CollectionUtils.isEmpty(registeredClient.scopes)) {
|
||||
this.scopes.addAll(registeredClient.scopes);
|
||||
}
|
||||
this.clientSettings = new ClientSettings(registeredClient.clientSettings.settings());
|
||||
this.tokenSettings = new TokenSettings(registeredClient.tokenSettings.settings());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -310,6 +365,30 @@ public class RegisteredClient implements Serializable {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Consumer} of the client configuration settings,
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param clientSettingsConsumer a {@link Consumer} of the client configuration settings
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder clientSettings(Consumer<ClientSettings> clientSettingsConsumer) {
|
||||
clientSettingsConsumer.accept(this.clientSettings);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Consumer} of the token configuration settings,
|
||||
* allowing the ability to add, replace, or remove.
|
||||
*
|
||||
* @param tokenSettingsConsumer a {@link Consumer} of the token configuration settings
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder tokenSettings(Consumer<TokenSettings> tokenSettingsConsumer) {
|
||||
tokenSettingsConsumer.accept(this.tokenSettings);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link RegisteredClient}.
|
||||
*
|
||||
|
@ -319,7 +398,6 @@ public class RegisteredClient implements Serializable {
|
|||
Assert.hasText(this.clientId, "clientId cannot be empty");
|
||||
Assert.notEmpty(this.authorizationGrantTypes, "authorizationGrantTypes cannot be empty");
|
||||
if (this.authorizationGrantTypes.contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
|
||||
Assert.hasText(this.clientSecret, "clientSecret cannot be empty");
|
||||
Assert.notEmpty(this.redirectUris, "redirectUris cannot be empty");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(this.clientAuthenticationMethods)) {
|
||||
|
@ -336,11 +414,16 @@ public class RegisteredClient implements Serializable {
|
|||
registeredClient.id = this.id;
|
||||
registeredClient.clientId = this.clientId;
|
||||
registeredClient.clientSecret = this.clientSecret;
|
||||
registeredClient.clientAuthenticationMethods =
|
||||
Collections.unmodifiableSet(this.clientAuthenticationMethods);
|
||||
registeredClient.authorizationGrantTypes = Collections.unmodifiableSet(this.authorizationGrantTypes);
|
||||
registeredClient.redirectUris = Collections.unmodifiableSet(this.redirectUris);
|
||||
registeredClient.scopes = Collections.unmodifiableSet(this.scopes);
|
||||
registeredClient.clientAuthenticationMethods = Collections.unmodifiableSet(
|
||||
new HashSet<>(this.clientAuthenticationMethods));
|
||||
registeredClient.authorizationGrantTypes = Collections.unmodifiableSet(
|
||||
new HashSet<>(this.authorizationGrantTypes));
|
||||
registeredClient.redirectUris = Collections.unmodifiableSet(
|
||||
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());
|
||||
|
||||
return registeredClient;
|
||||
}
|
||||
|
|
|
@ -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,6 +15,8 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.client;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A repository for OAuth 2.0 {@link RegisteredClient}(s).
|
||||
*
|
||||
|
@ -26,19 +28,23 @@ package org.springframework.security.oauth2.server.authorization.client;
|
|||
public interface RegisteredClientRepository {
|
||||
|
||||
/**
|
||||
* Returns the registered client identified by the provided {@code id}, or {@code null} if not found.
|
||||
* Returns the registered client identified by the provided {@code id},
|
||||
* or {@code null} if not found.
|
||||
*
|
||||
* @param id the registration identifier
|
||||
* @return the {@link RegisteredClient} if found, otherwise {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
RegisteredClient findById(String id);
|
||||
|
||||
/**
|
||||
* Returns the registered client identified by the provided {@code clientId}, or {@code null} if not found.
|
||||
* Returns the registered client identified by the provided {@code clientId},
|
||||
* or {@code null} if not found.
|
||||
*
|
||||
* @param clientId the client identifier
|
||||
* @return the {@link RegisteredClient} if found, otherwise {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
RegisteredClient findByClientId(String clientId);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A facility for client configuration settings.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.2
|
||||
* @see Settings
|
||||
*/
|
||||
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");
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the client is required to provide a proof key challenge and verifier
|
||||
* when performing the Authorization Code Grant flow. The default is {@code false}.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* The default is {@code false}.
|
||||
*
|
||||
* @return {@code true} if the user's consent is required when the client requests access, {@code false} otherwise
|
||||
*/
|
||||
public boolean requireUserConsent() {
|
||||
return setting(REQUIRE_USER_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}).
|
||||
*
|
||||
* @param requireUserConsent {@code true} if the user's consent is required when the client requests access, {@code false} otherwise
|
||||
* @return the {@link ClientSettings}
|
||||
*/
|
||||
public ClientSettings requireUserConsent(boolean requireUserConsent) {
|
||||
setting(REQUIRE_USER_CONSENT, requireUserConsent);
|
||||
return this;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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 java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A facility for provider configuration settings.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.1.0
|
||||
* @see Settings
|
||||
*/
|
||||
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");
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the Provider's Issuer Identifier
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's OAuth 2.0 Authorization endpoint. The default is {@code /oauth2/authorize}.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's OAuth 2.0 Token endpoint. The default is {@code /oauth2/token}.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's JWK Set endpoint. The default is {@code /oauth2/jwks}.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's OAuth 2.0 Token Revocation endpoint. The default is {@code /oauth2/revoke}.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
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");
|
||||
return settings;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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.util.Assert;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A facility for token configuration settings.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.2
|
||||
* @see Settings
|
||||
*/
|
||||
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");
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time-to-live for an access token. The default is 5 minutes.
|
||||
*
|
||||
* @return the time-to-live for an access token
|
||||
*/
|
||||
public Duration accessTokenTimeToLive() {
|
||||
return setting(ACCESS_TOKEN_TIME_TO_LIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 TokenSettings}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time-to-live for a refresh token. The default is 60 minutes.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
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));
|
||||
return settings;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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.oidc.web;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcProviderConfiguration;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.http.converter.OidcProviderConfigurationHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@code Filter} that processes OpenID Provider Configuration Requests.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.1.0
|
||||
* @see OidcProviderConfiguration
|
||||
* @see ProviderSettings
|
||||
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">4.1. OpenID Provider Configuration Request</a>
|
||||
*/
|
||||
public class OidcProviderConfigurationEndpointFilter extends OncePerRequestFilter {
|
||||
/**
|
||||
* The default endpoint {@code URI} for OpenID Provider Configuration requests.
|
||||
*/
|
||||
public static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration";
|
||||
|
||||
private final ProviderSettings providerSettings;
|
||||
private final RequestMatcher requestMatcher;
|
||||
private final OidcProviderConfigurationHttpMessageConverter providerConfigurationHttpMessageConverter =
|
||||
new OidcProviderConfigurationHttpMessageConverter();
|
||||
|
||||
public OidcProviderConfigurationEndpointFilter(ProviderSettings providerSettings) {
|
||||
Assert.notNull(providerSettings, "providerSettings cannot be null");
|
||||
this.providerSettings = providerSettings;
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI,
|
||||
HttpMethod.GET.name()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (!this.requestMatcher.matches(request)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.builder()
|
||||
.issuer(this.providerSettings.issuer())
|
||||
.authorizationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.authorizationEndpoint()))
|
||||
.tokenEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenEndpoint()))
|
||||
.tokenEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
|
||||
.tokenEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
|
||||
.jwkSetUri(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint()))
|
||||
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
|
||||
.subjectType("public")
|
||||
.idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName())
|
||||
.scope(OidcScopes.OPENID)
|
||||
.build();
|
||||
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
this.providerConfigurationHttpMessageConverter.write(
|
||||
providerConfiguration, MediaType.APPLICATION_JSON, httpResponse);
|
||||
}
|
||||
|
||||
private static String asUrl(String issuer, String endpoint) {
|
||||
return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString();
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.authorization.web;
|
|||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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;
|
||||
|
@ -28,6 +29,9 @@ import javax.servlet.http.HttpServletRequest;
|
|||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Attempts to extract HTTP Basic credentials from {@link HttpServletRequest}
|
||||
|
@ -82,6 +86,16 @@ public class ClientSecretBasicAuthenticationConverter implements AuthenticationC
|
|||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);
|
||||
}
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(clientID, clientSecret);
|
||||
return new OAuth2ClientAuthenticationToken(clientID, clientSecret, ClientAuthenticationMethod.BASIC,
|
||||
extractAdditionalParameters(request));
|
||||
}
|
||||
|
||||
private static Map<String, Object> extractAdditionalParameters(HttpServletRequest request) {
|
||||
Map<String, Object> additionalParameters = Collections.emptyMap();
|
||||
if (OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
|
||||
// Confidential clients can also leverage PKCE
|
||||
additionalParameters = new HashMap<>(OAuth2EndpointUtils.getParameters(request).toSingleValueMap());
|
||||
}
|
||||
return additionalParameters;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.web;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Attempts to extract client credentials from POST parameters of {@link HttpServletRequest}
|
||||
* and then converts to an {@link OAuth2ClientAuthenticationToken} used for authenticating the client.
|
||||
*
|
||||
* @author Anoop Garlapati
|
||||
* @since 0.1.0
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see OAuth2ClientAuthenticationFilter
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2.3.1">Section 2.3.1 Client Password</a>
|
||||
*/
|
||||
public class ClientSecretPostAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
// client_id (REQUIRED)
|
||||
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
|
||||
if (!StringUtils.hasText(clientId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
|
||||
}
|
||||
|
||||
// client_secret (REQUIRED)
|
||||
String clientSecret = parameters.getFirst(OAuth2ParameterNames.CLIENT_SECRET);
|
||||
if (!StringUtils.hasText(clientSecret)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parameters.get(OAuth2ParameterNames.CLIENT_SECRET).size() != 1) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
|
||||
}
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(clientId, clientSecret, ClientAuthenticationMethod.POST,
|
||||
extractAdditionalParameters(request));
|
||||
}
|
||||
|
||||
private static Map<String, Object> extractAdditionalParameters(HttpServletRequest request) {
|
||||
Map<String, Object> additionalParameters = Collections.emptyMap();
|
||||
if (OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
|
||||
// Confidential clients can also leverage PKCE
|
||||
additionalParameters = new HashMap<>(OAuth2EndpointUtils.getParameters(request).toSingleValueMap());
|
||||
additionalParameters.remove(OAuth2ParameterNames.CLIENT_ID);
|
||||
additionalParameters.remove(OAuth2ParameterNames.CLIENT_SECRET);
|
||||
}
|
||||
return additionalParameters;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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.web;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationConverter} that simply delegates to it's
|
||||
* internal {@code List} of {@link AuthenticationConverter}(s).
|
||||
* <p>
|
||||
* Each {@link AuthenticationConverter} is given a chance to
|
||||
* {@link AuthenticationConverter#convert(HttpServletRequest)}
|
||||
* with the first {@code non-null} {@link Authentication} being returned.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.2
|
||||
* @see AuthenticationConverter
|
||||
*/
|
||||
public final class DelegatingAuthenticationConverter implements AuthenticationConverter {
|
||||
private final List<AuthenticationConverter> converters;
|
||||
|
||||
/**
|
||||
* Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters.
|
||||
*
|
||||
* @param converters a {@code List} of {@link AuthenticationConverter}(s)
|
||||
*/
|
||||
public DelegatingAuthenticationConverter(List<AuthenticationConverter> converters) {
|
||||
Assert.notEmpty(converters, "converters cannot be empty");
|
||||
this.converters = Collections.unmodifiableList(new LinkedList<>(converters));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
Assert.notNull(request, "request cannot be null");
|
||||
// @formatter:off
|
||||
return this.converters.stream()
|
||||
.map(converter -> converter.convert(request))
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
|
@ -1,68 +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.web;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link Converter} that selects (and delegates) to one of the internal {@code Map} of {@link Converter}'s
|
||||
* using the {@link OAuth2ParameterNames#GRANT_TYPE} request parameter.
|
||||
*
|
||||
* @author Alexey Nesterov
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public final class DelegatingAuthorizationGrantAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
|
||||
private final Map<AuthorizationGrantType, Converter<HttpServletRequest, Authentication>> converters;
|
||||
|
||||
/**
|
||||
* Constructs a {@code DelegatingAuthorizationGrantAuthenticationConverter} using the provided parameters.
|
||||
*
|
||||
* @param converters a {@code Map} of {@link Converter}(s)
|
||||
*/
|
||||
public DelegatingAuthorizationGrantAuthenticationConverter(
|
||||
Map<AuthorizationGrantType, Converter<HttpServletRequest, Authentication>> converters) {
|
||||
Assert.notEmpty(converters, "converters cannot be empty");
|
||||
this.converters = Collections.unmodifiableMap(new HashMap<>(converters));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
Assert.notNull(request, "request cannot be null");
|
||||
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (StringUtils.isEmpty(grantType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Converter<HttpServletRequest, Authentication> converter =
|
||||
this.converters.get(new AuthorizationGrantType(grantType));
|
||||
if (converter == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return converter.convert(request);
|
||||
}
|
||||
}
|
|
@ -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,70 +15,65 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.web;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.jwk.Curve;
|
||||
import com.nimbusds.jose.jwk.ECKey;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.KeyUse;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.crypto.keys.KeyManager;
|
||||
import org.springframework.security.crypto.keys.ManagedKey;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKMatcher;
|
||||
import com.nimbusds.jose.jwk.JWKSelector;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* A {@code Filter} that processes JWK Set requests.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
* @see KeyManager
|
||||
* @see com.nimbusds.jose.jwk.source.JWKSource
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517#section-5">Section 5 JWK Set Format</a>
|
||||
*/
|
||||
public class JwkSetEndpointFilter extends OncePerRequestFilter {
|
||||
public class NimbusJwkSetEndpointFilter extends OncePerRequestFilter {
|
||||
/**
|
||||
* The default endpoint {@code URI} for JWK Set requests.
|
||||
*/
|
||||
public static final String DEFAULT_JWK_SET_ENDPOINT_URI = "/oauth2/jwks";
|
||||
|
||||
private final KeyManager keyManager;
|
||||
private final JWKSource<SecurityContext> jwkSource;
|
||||
private final JWKSelector jwkSelector;
|
||||
private final RequestMatcher requestMatcher;
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwkSetEndpointFilter} using the provided parameters.
|
||||
*
|
||||
* @param keyManager the key manager
|
||||
* Constructs a {@code NimbusJwkSetEndpointFilter} using the provided parameters.
|
||||
* @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
|
||||
*/
|
||||
public JwkSetEndpointFilter(KeyManager keyManager) {
|
||||
this(keyManager, DEFAULT_JWK_SET_ENDPOINT_URI);
|
||||
public NimbusJwkSetEndpointFilter(JWKSource<SecurityContext> jwkSource) {
|
||||
this(jwkSource, DEFAULT_JWK_SET_ENDPOINT_URI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwkSetEndpointFilter} using the provided parameters.
|
||||
* Constructs a {@code NimbusJwkSetEndpointFilter} using the provided parameters.
|
||||
*
|
||||
* @param keyManager the key manager
|
||||
* @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
|
||||
* @param jwkSetEndpointUri the endpoint {@code URI} for JWK Set requests
|
||||
*/
|
||||
public JwkSetEndpointFilter(KeyManager keyManager, String jwkSetEndpointUri) {
|
||||
Assert.notNull(keyManager, "keyManager cannot be null");
|
||||
public NimbusJwkSetEndpointFilter(JWKSource<SecurityContext> jwkSource, String jwkSetEndpointUri) {
|
||||
Assert.notNull(jwkSource, "jwkSource cannot be null");
|
||||
Assert.hasText(jwkSetEndpointUri, "jwkSetEndpointUri cannot be empty");
|
||||
this.keyManager = keyManager;
|
||||
this.jwkSource = jwkSource;
|
||||
this.jwkSelector = new JWKSelector(new JWKMatcher.Builder().build());
|
||||
this.requestMatcher = new AntPathRequestMatcher(jwkSetEndpointUri, HttpMethod.GET.name());
|
||||
}
|
||||
|
||||
|
@ -91,42 +86,17 @@ public class JwkSetEndpointFilter extends OncePerRequestFilter {
|
|||
return;
|
||||
}
|
||||
|
||||
JWKSet jwkSet = buildJwkSet();
|
||||
JWKSet jwkSet;
|
||||
try {
|
||||
jwkSet = new JWKSet(this.jwkSource.get(this.jwkSelector, null));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to select the JWK(s) -> " + ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
try (Writer writer = response.getWriter()) {
|
||||
writer.write(jwkSet.toJSONObject().toString());
|
||||
writer.write(jwkSet.toString()); // toString() excludes private keys
|
||||
}
|
||||
}
|
||||
|
||||
private JWKSet buildJwkSet() {
|
||||
return new JWKSet(
|
||||
this.keyManager.getKeys().stream()
|
||||
.filter(managedKey -> managedKey.isActive() && managedKey.isAsymmetric())
|
||||
.map(this::convert)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
|
||||
private JWK convert(ManagedKey managedKey) {
|
||||
JWK jwk = null;
|
||||
if (managedKey.getPublicKey() instanceof RSAPublicKey) {
|
||||
RSAPublicKey publicKey = (RSAPublicKey) managedKey.getPublicKey();
|
||||
jwk = new RSAKey.Builder(publicKey)
|
||||
.keyUse(KeyUse.SIGNATURE)
|
||||
.algorithm(JWSAlgorithm.RS256)
|
||||
.keyID(managedKey.getKeyId())
|
||||
.build();
|
||||
} else if (managedKey.getPublicKey() instanceof ECPublicKey) {
|
||||
ECPublicKey publicKey = (ECPublicKey) managedKey.getPublicKey();
|
||||
Curve curve = Curve.forECParameterSpec(publicKey.getParams());
|
||||
jwk = new ECKey.Builder(curve, publicKey)
|
||||
.keyUse(KeyUse.SIGNATURE)
|
||||
.algorithm(JWSAlgorithm.ES256)
|
||||
.keyID(managedKey.getKeyId())
|
||||
.build();
|
||||
}
|
||||
return jwk;
|
||||
}
|
||||
}
|
|
@ -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,8 +15,26 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
@ -25,47 +43,45 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
|||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
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.OAuth2AuthorizationResponseType;
|
||||
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.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
|
||||
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.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
|
||||
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.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
|
||||
* which handles the processing of the OAuth 2.0 Authorization Request.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Paurav Munshi
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.0.1
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2Authorization
|
||||
* @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.1">Section 4.1.1 Authorization Request</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
|
||||
*/
|
||||
public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
||||
/**
|
||||
|
@ -73,10 +89,15 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|||
*/
|
||||
public static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
|
||||
|
||||
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
|
||||
private static final String PKCE_ERROR_URI = "https://tools.ietf.org/html/rfc7636#section-4.4.1";
|
||||
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final RequestMatcher authorizationEndpointMatcher;
|
||||
private final StringKeyGenerator codeGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
||||
private final RequestMatcher authorizationRequestMatcher;
|
||||
private final RequestMatcher userConsentMatcher;
|
||||
private final StringKeyGenerator codeGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
||||
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
||||
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
||||
|
||||
/**
|
||||
|
@ -104,73 +125,56 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|||
Assert.hasText(authorizationEndpointUri, "authorizationEndpointUri cannot be empty");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.authorizationService = authorizationService;
|
||||
this.authorizationEndpointMatcher = new AntPathRequestMatcher(
|
||||
|
||||
RequestMatcher authorizationRequestGetMatcher = new AntPathRequestMatcher(
|
||||
authorizationEndpointUri, HttpMethod.GET.name());
|
||||
RequestMatcher authorizationRequestPostMatcher = new AntPathRequestMatcher(
|
||||
authorizationEndpointUri, HttpMethod.POST.name());
|
||||
RequestMatcher openidScopeMatcher = request -> {
|
||||
String scope = request.getParameter(OAuth2ParameterNames.SCOPE);
|
||||
return StringUtils.hasText(scope) && scope.contains(OidcScopes.OPENID);
|
||||
};
|
||||
RequestMatcher consentActionMatcher = request ->
|
||||
request.getParameter(UserConsentPage.CONSENT_ACTION_PARAMETER_NAME) != null;
|
||||
this.authorizationRequestMatcher = new OrRequestMatcher(
|
||||
authorizationRequestGetMatcher,
|
||||
new AndRequestMatcher(
|
||||
authorizationRequestPostMatcher, openidScopeMatcher,
|
||||
new NegatedRequestMatcher(consentActionMatcher)));
|
||||
this.userConsentMatcher = new AndRequestMatcher(
|
||||
authorizationRequestPostMatcher, consentActionMatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (!this.authorizationEndpointMatcher.matches(request)) {
|
||||
if (this.authorizationRequestMatcher.matches(request)) {
|
||||
processAuthorizationRequest(request, response, filterChain);
|
||||
} else if (this.userConsentMatcher.matches(request)) {
|
||||
processUserConsent(request, response);
|
||||
} else {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Validate the request to ensure that all required parameters are present and valid
|
||||
// ---------------
|
||||
private void processAuthorizationRequest(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
String stateParameter = parameters.getFirst(OAuth2ParameterNames.STATE);
|
||||
OAuth2AuthorizationRequestContext authorizationRequestContext =
|
||||
new OAuth2AuthorizationRequestContext(
|
||||
request.getRequestURL().toString(),
|
||||
OAuth2EndpointUtils.getParameters(request));
|
||||
|
||||
// client_id (REQUIRED)
|
||||
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
|
||||
if (!StringUtils.hasText(clientId) ||
|
||||
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
|
||||
OAuth2Error error = createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
|
||||
sendErrorResponse(request, response, error, stateParameter, null); // when redirectUri is null then don't redirect
|
||||
return;
|
||||
}
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
|
||||
if (registeredClient == null) {
|
||||
OAuth2Error error = createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
|
||||
sendErrorResponse(request, response, error, stateParameter, null); // when redirectUri is null then don't redirect
|
||||
return;
|
||||
} else if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
|
||||
OAuth2Error error = createError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID);
|
||||
sendErrorResponse(request, response, error, stateParameter, null); // when redirectUri is null then don't redirect
|
||||
return;
|
||||
}
|
||||
validateAuthorizationRequest(authorizationRequestContext);
|
||||
|
||||
// redirect_uri (OPTIONAL)
|
||||
String redirectUriParameter = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
|
||||
if (StringUtils.hasText(redirectUriParameter)) {
|
||||
if (!registeredClient.getRedirectUris().contains(redirectUriParameter) ||
|
||||
parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {
|
||||
OAuth2Error error = createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
|
||||
sendErrorResponse(request, response, error, stateParameter, null); // when redirectUri is null then don't redirect
|
||||
return;
|
||||
if (authorizationRequestContext.hasError()) {
|
||||
if (authorizationRequestContext.isRedirectOnError()) {
|
||||
sendErrorResponse(request, response, authorizationRequestContext.resolveRedirectUri(),
|
||||
authorizationRequestContext.getError(), authorizationRequestContext.getState());
|
||||
} else {
|
||||
sendErrorResponse(response, authorizationRequestContext.getError());
|
||||
}
|
||||
} else if (registeredClient.getRedirectUris().size() != 1) {
|
||||
OAuth2Error error = createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
|
||||
sendErrorResponse(request, response, error, stateParameter, null); // when redirectUri is null then don't redirect
|
||||
return;
|
||||
}
|
||||
|
||||
String redirectUri = StringUtils.hasText(redirectUriParameter) ?
|
||||
redirectUriParameter : registeredClient.getRedirectUris().iterator().next();
|
||||
|
||||
// response_type (REQUIRED)
|
||||
String responseType = parameters.getFirst(OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
if (!StringUtils.hasText(responseType) ||
|
||||
parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
|
||||
OAuth2Error error = createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
sendErrorResponse(request, response, error, stateParameter, redirectUri);
|
||||
return;
|
||||
} else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
|
||||
OAuth2Error error = createError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
sendErrorResponse(request, response, error, stateParameter, redirectUri);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -186,48 +190,264 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|||
return;
|
||||
}
|
||||
|
||||
String code = this.codeGenerator.generateKey();
|
||||
OAuth2AuthorizationRequest authorizationRequest = convertAuthorizationRequest(request);
|
||||
|
||||
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
RegisteredClient registeredClient = authorizationRequestContext.getRegisteredClient();
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorizationRequestContext.buildAuthorizationRequest();
|
||||
OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.principalName(principal.getName())
|
||||
.attribute(OAuth2AuthorizationAttributeNames.CODE, code)
|
||||
.attribute(OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST, authorizationRequest)
|
||||
.build();
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.attribute(Principal.class.getName(), principal)
|
||||
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
|
||||
|
||||
if (requireUserConsent(registeredClient, authorizationRequest)) {
|
||||
String state = this.stateGenerator.generateKey();
|
||||
OAuth2Authorization authorization = builder
|
||||
.attribute(OAuth2ParameterNames.STATE, state)
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
// TODO Need to remove 'in-flight' authorization if consent step is not completed (e.g. approved or cancelled)
|
||||
|
||||
UserConsentPage.displayConsent(request, response, registeredClient, authorization);
|
||||
} else {
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
|
||||
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
|
||||
this.codeGenerator.generateKey(), issuedAt, expiresAt);
|
||||
OAuth2Authorization authorization = builder
|
||||
.token(authorizationCode)
|
||||
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes())
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
// TODO security checks for code parameter
|
||||
// The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks.
|
||||
// A maximum authorization code lifetime of 10 minutes is RECOMMENDED.
|
||||
// The client MUST NOT use the authorization code more than once.
|
||||
// If an authorization code is used more than once, the authorization server MUST deny the request
|
||||
// and SHOULD revoke (when possible) all tokens previously issued based on that authorization code.
|
||||
// The authorization code is bound to the client identifier and redirection URI.
|
||||
|
||||
sendAuthorizationResponse(request, response,
|
||||
authorizationRequestContext.resolveRedirectUri(), authorizationCode, authorizationRequest.getState());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean requireUserConsent(RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||
// openid scope does not require consent
|
||||
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID) &&
|
||||
authorizationRequest.getScopes().size() == 1) {
|
||||
return false;
|
||||
}
|
||||
return registeredClient.getClientSettings().requireUserConsent();
|
||||
}
|
||||
|
||||
private void processUserConsent(HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException {
|
||||
|
||||
UserConsentRequestContext userConsentRequestContext =
|
||||
new UserConsentRequestContext(
|
||||
request.getRequestURL().toString(),
|
||||
OAuth2EndpointUtils.getParameters(request));
|
||||
|
||||
validateUserConsentRequest(userConsentRequestContext);
|
||||
|
||||
if (userConsentRequestContext.hasError()) {
|
||||
if (userConsentRequestContext.isRedirectOnError()) {
|
||||
sendErrorResponse(request, response, userConsentRequestContext.resolveRedirectUri(),
|
||||
userConsentRequestContext.getError(), userConsentRequestContext.getState());
|
||||
} else {
|
||||
sendErrorResponse(response, userConsentRequestContext.getError());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!UserConsentPage.isConsentApproved(request)) {
|
||||
this.authorizationService.remove(userConsentRequestContext.getAuthorization());
|
||||
OAuth2Error error = createError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID);
|
||||
sendErrorResponse(request, response, userConsentRequestContext.resolveRedirectUri(),
|
||||
error, userConsentRequestContext.getAuthorizationRequest().getState());
|
||||
return;
|
||||
}
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
|
||||
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
|
||||
this.codeGenerator.generateKey(), issuedAt, expiresAt);
|
||||
Set<String> authorizedScopes = userConsentRequestContext.getScopes();
|
||||
if (userConsentRequestContext.getAuthorizationRequest().getScopes().contains(OidcScopes.OPENID)) {
|
||||
// openid scope is auto-approved as it does not require consent
|
||||
authorizedScopes.add(OidcScopes.OPENID);
|
||||
}
|
||||
OAuth2Authorization authorization = OAuth2Authorization.from(userConsentRequestContext.getAuthorization())
|
||||
.token(authorizationCode)
|
||||
.attributes(attrs -> {
|
||||
attrs.remove(OAuth2ParameterNames.STATE);
|
||||
attrs.put(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
|
||||
})
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
// TODO security checks for code parameter
|
||||
// The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks.
|
||||
// A maximum authorization code lifetime of 10 minutes is RECOMMENDED.
|
||||
// The client MUST NOT use the authorization code more than once.
|
||||
// If an authorization code is used more than once, the authorization server MUST deny the request
|
||||
// and SHOULD revoke (when possible) all tokens previously issued based on that authorization code.
|
||||
// The authorization code is bound to the client identifier and redirection URI.
|
||||
sendAuthorizationResponse(request, response, userConsentRequestContext.resolveRedirectUri(),
|
||||
authorizationCode, userConsentRequestContext.getAuthorizationRequest().getState());
|
||||
}
|
||||
|
||||
sendAuthorizationResponse(request, response, authorizationRequest, code, redirectUri);
|
||||
private void validateAuthorizationRequest(OAuth2AuthorizationRequestContext authorizationRequestContext) {
|
||||
// ---------------
|
||||
// Validate the request to ensure all required parameters are present and valid
|
||||
// ---------------
|
||||
|
||||
// client_id (REQUIRED)
|
||||
if (!StringUtils.hasText(authorizationRequestContext.getClientId()) ||
|
||||
authorizationRequestContext.getParameters().get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
|
||||
authorizationRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID));
|
||||
return;
|
||||
}
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
authorizationRequestContext.getClientId());
|
||||
if (registeredClient == null) {
|
||||
authorizationRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID));
|
||||
return;
|
||||
} else if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
|
||||
authorizationRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID));
|
||||
return;
|
||||
}
|
||||
authorizationRequestContext.setRegisteredClient(registeredClient);
|
||||
|
||||
// redirect_uri (OPTIONAL)
|
||||
if (StringUtils.hasText(authorizationRequestContext.getRedirectUri())) {
|
||||
if (!registeredClient.getRedirectUris().contains(authorizationRequestContext.getRedirectUri()) ||
|
||||
authorizationRequestContext.getParameters().get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {
|
||||
authorizationRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI));
|
||||
return;
|
||||
}
|
||||
} else if (authorizationRequestContext.isAuthenticationRequest() || // redirect_uri is REQUIRED for OpenID Connect
|
||||
registeredClient.getRedirectUris().size() != 1) {
|
||||
authorizationRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI));
|
||||
return;
|
||||
}
|
||||
authorizationRequestContext.setRedirectOnError(true);
|
||||
|
||||
// response_type (REQUIRED)
|
||||
if (!StringUtils.hasText(authorizationRequestContext.getResponseType()) ||
|
||||
authorizationRequestContext.getParameters().get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
|
||||
authorizationRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE));
|
||||
return;
|
||||
} else if (!authorizationRequestContext.getResponseType().equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
|
||||
authorizationRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE));
|
||||
return;
|
||||
}
|
||||
|
||||
// scope (OPTIONAL)
|
||||
Set<String> requestedScopes = authorizationRequestContext.getScopes();
|
||||
Set<String> allowedScopes = registeredClient.getScopes();
|
||||
if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
|
||||
authorizationRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE));
|
||||
return;
|
||||
}
|
||||
|
||||
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
|
||||
String codeChallenge = authorizationRequestContext.getParameters().getFirst(PkceParameterNames.CODE_CHALLENGE);
|
||||
if (StringUtils.hasText(codeChallenge)) {
|
||||
if (authorizationRequestContext.getParameters().get(PkceParameterNames.CODE_CHALLENGE).size() != 1) {
|
||||
authorizationRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI));
|
||||
return;
|
||||
}
|
||||
|
||||
String codeChallengeMethod = authorizationRequestContext.getParameters().getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD);
|
||||
if (StringUtils.hasText(codeChallengeMethod)) {
|
||||
if (authorizationRequestContext.getParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1 ||
|
||||
(!"S256".equals(codeChallengeMethod) && !"plain".equals(codeChallengeMethod))) {
|
||||
authorizationRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (registeredClient.getClientSettings().requireProofKey()) {
|
||||
authorizationRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void validateUserConsentRequest(UserConsentRequestContext userConsentRequestContext) {
|
||||
// ---------------
|
||||
// Validate the request to ensure all required parameters are present and valid
|
||||
// ---------------
|
||||
|
||||
// state (REQUIRED)
|
||||
if (!StringUtils.hasText(userConsentRequestContext.getState()) ||
|
||||
userConsentRequestContext.getParameters().get(OAuth2ParameterNames.STATE).size() != 1) {
|
||||
userConsentRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE));
|
||||
return;
|
||||
}
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
userConsentRequestContext.getState(), STATE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
userConsentRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE));
|
||||
return;
|
||||
}
|
||||
userConsentRequestContext.setAuthorization(authorization);
|
||||
|
||||
// The 'in-flight' authorization must be associated to the current principal
|
||||
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
|
||||
userConsentRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE));
|
||||
return;
|
||||
}
|
||||
|
||||
// client_id (REQUIRED)
|
||||
if (!StringUtils.hasText(userConsentRequestContext.getClientId()) ||
|
||||
userConsentRequestContext.getParameters().get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
|
||||
userConsentRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID));
|
||||
return;
|
||||
}
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
userConsentRequestContext.getClientId());
|
||||
if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
userConsentRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID));
|
||||
return;
|
||||
}
|
||||
userConsentRequestContext.setRegisteredClient(registeredClient);
|
||||
userConsentRequestContext.setRedirectOnError(true);
|
||||
|
||||
// scope (OPTIONAL)
|
||||
Set<String> requestedScopes = userConsentRequestContext.getAuthorizationRequest().getScopes();
|
||||
Set<String> authorizedScopes = userConsentRequestContext.getScopes();
|
||||
if (!authorizedScopes.isEmpty() && !requestedScopes.containsAll(authorizedScopes)) {
|
||||
userConsentRequestContext.setError(
|
||||
createError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendAuthorizationResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
OAuth2AuthorizationRequest authorizationRequest, String code, String redirectUri) throws IOException {
|
||||
String redirectUri, OAuth2AuthorizationCode authorizationCode, String state) throws IOException {
|
||||
|
||||
UriComponentsBuilder uriBuilder = UriComponentsBuilder
|
||||
.fromUriString(redirectUri)
|
||||
.queryParam(OAuth2ParameterNames.CODE, code);
|
||||
if (StringUtils.hasText(authorizationRequest.getState())) {
|
||||
uriBuilder.queryParam(OAuth2ParameterNames.STATE, authorizationRequest.getState());
|
||||
.queryParam(OAuth2ParameterNames.CODE, authorizationCode.getTokenValue());
|
||||
if (StringUtils.hasText(state)) {
|
||||
uriBuilder.queryParam(OAuth2ParameterNames.STATE, state);
|
||||
}
|
||||
this.redirectStrategy.sendRedirect(request, response, uriBuilder.toUriString());
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
OAuth2Error error, String state, String redirectUri) throws IOException {
|
||||
|
||||
if (redirectUri == null) {
|
||||
// TODO Send default html error response
|
||||
response.sendError(HttpStatus.BAD_REQUEST.value(), error.toString());
|
||||
return;
|
||||
}
|
||||
String redirectUri, OAuth2Error error, String state) throws IOException {
|
||||
|
||||
UriComponentsBuilder uriBuilder = UriComponentsBuilder
|
||||
.fromUriString(redirectUri)
|
||||
|
@ -244,9 +464,17 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|||
this.redirectStrategy.sendRedirect(request, response, uriBuilder.toUriString());
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
|
||||
// TODO Send default html error response
|
||||
response.sendError(HttpStatus.BAD_REQUEST.value(), error.toString());
|
||||
}
|
||||
|
||||
private static OAuth2Error createError(String errorCode, String parameterName) {
|
||||
return new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName,
|
||||
"https://tools.ietf.org/html/rfc6749#section-4.1.2.1");
|
||||
return createError(errorCode, parameterName, "https://tools.ietf.org/html/rfc6749#section-4.1.2.1");
|
||||
}
|
||||
|
||||
private static OAuth2Error createError(String errorCode, String parameterName, String errorUri) {
|
||||
return new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
|
||||
}
|
||||
|
||||
private static boolean isPrincipalAuthenticated(Authentication principal) {
|
||||
|
@ -255,29 +483,260 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
|
|||
principal.isAuthenticated();
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationRequest convertAuthorizationRequest(HttpServletRequest request) {
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
private static class OAuth2AuthorizationRequestContext extends AbstractRequestContext {
|
||||
private final String responseType;
|
||||
private final String redirectUri;
|
||||
|
||||
Set<String> scopes = Collections.emptySet();
|
||||
if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) {
|
||||
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
|
||||
scopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
|
||||
private OAuth2AuthorizationRequestContext(
|
||||
String authorizationUri, MultiValueMap<String, String> parameters) {
|
||||
super(authorizationUri, parameters,
|
||||
parameters.getFirst(OAuth2ParameterNames.CLIENT_ID),
|
||||
parameters.getFirst(OAuth2ParameterNames.STATE),
|
||||
extractScopes(parameters));
|
||||
this.responseType = parameters.getFirst(OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
this.redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
|
||||
}
|
||||
|
||||
return OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri(request.getRequestURL().toString())
|
||||
.clientId(parameters.getFirst(OAuth2ParameterNames.CLIENT_ID))
|
||||
.redirectUri(parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI))
|
||||
.scopes(scopes)
|
||||
.state(parameters.getFirst(OAuth2ParameterNames.STATE))
|
||||
.additionalParameters(additionalParameters ->
|
||||
parameters.entrySet().stream()
|
||||
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.RESPONSE_TYPE) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.SCOPE) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.STATE))
|
||||
.forEach(e -> additionalParameters.put(e.getKey(), e.getValue().get(0))))
|
||||
.build();
|
||||
private static Set<String> extractScopes(MultiValueMap<String, String> parameters) {
|
||||
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
|
||||
return StringUtils.hasText(scope) ?
|
||||
new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))) :
|
||||
Collections.emptySet();
|
||||
}
|
||||
|
||||
private String getResponseType() {
|
||||
return this.responseType;
|
||||
}
|
||||
|
||||
private String getRedirectUri() {
|
||||
return this.redirectUri;
|
||||
}
|
||||
|
||||
private boolean isAuthenticationRequest() {
|
||||
return getScopes().contains(OidcScopes.OPENID);
|
||||
}
|
||||
|
||||
protected String resolveRedirectUri() {
|
||||
return StringUtils.hasText(getRedirectUri()) ?
|
||||
getRedirectUri() :
|
||||
getRegisteredClient().getRedirectUris().iterator().next();
|
||||
}
|
||||
|
||||
private OAuth2AuthorizationRequest buildAuthorizationRequest() {
|
||||
return OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri(getAuthorizationUri())
|
||||
.clientId(getClientId())
|
||||
.redirectUri(getRedirectUri())
|
||||
.scopes(getScopes())
|
||||
.state(getState())
|
||||
.additionalParameters(additionalParameters ->
|
||||
getParameters().entrySet().stream()
|
||||
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.RESPONSE_TYPE) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.SCOPE) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.STATE))
|
||||
.forEach(e -> additionalParameters.put(e.getKey(), e.getValue().get(0))))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static class UserConsentRequestContext extends AbstractRequestContext {
|
||||
private OAuth2Authorization authorization;
|
||||
|
||||
private UserConsentRequestContext(
|
||||
String authorizationUri, MultiValueMap<String, String> parameters) {
|
||||
super(authorizationUri, parameters,
|
||||
parameters.getFirst(OAuth2ParameterNames.CLIENT_ID),
|
||||
parameters.getFirst(OAuth2ParameterNames.STATE),
|
||||
extractScopes(parameters));
|
||||
}
|
||||
|
||||
private static Set<String> extractScopes(MultiValueMap<String, String> parameters) {
|
||||
List<String> scope = parameters.get(OAuth2ParameterNames.SCOPE);
|
||||
return !CollectionUtils.isEmpty(scope) ? new HashSet<>(scope) : Collections.emptySet();
|
||||
}
|
||||
|
||||
private OAuth2Authorization getAuthorization() {
|
||||
return this.authorization;
|
||||
}
|
||||
|
||||
private void setAuthorization(OAuth2Authorization authorization) {
|
||||
this.authorization = authorization;
|
||||
}
|
||||
|
||||
protected String resolveRedirectUri() {
|
||||
OAuth2AuthorizationRequest authorizationRequest = getAuthorizationRequest();
|
||||
return StringUtils.hasText(authorizationRequest.getRedirectUri()) ?
|
||||
authorizationRequest.getRedirectUri() :
|
||||
getRegisteredClient().getRedirectUris().iterator().next();
|
||||
}
|
||||
|
||||
private OAuth2AuthorizationRequest getAuthorizationRequest() {
|
||||
return getAuthorization().getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class AbstractRequestContext {
|
||||
private final String authorizationUri;
|
||||
private final MultiValueMap<String, String> parameters;
|
||||
private final String clientId;
|
||||
private final String state;
|
||||
private final Set<String> scopes;
|
||||
private RegisteredClient registeredClient;
|
||||
private OAuth2Error error;
|
||||
private boolean redirectOnError;
|
||||
|
||||
protected AbstractRequestContext(String authorizationUri, MultiValueMap<String, String> parameters,
|
||||
String clientId, String state, Set<String> scopes) {
|
||||
this.authorizationUri = authorizationUri;
|
||||
this.parameters = parameters;
|
||||
this.clientId = clientId;
|
||||
this.state = state;
|
||||
this.scopes = scopes;
|
||||
}
|
||||
|
||||
protected String getAuthorizationUri() {
|
||||
return this.authorizationUri;
|
||||
}
|
||||
|
||||
protected MultiValueMap<String, String> getParameters() {
|
||||
return this.parameters;
|
||||
}
|
||||
|
||||
protected String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
protected String getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
protected Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
protected RegisteredClient getRegisteredClient() {
|
||||
return this.registeredClient;
|
||||
}
|
||||
|
||||
protected void setRegisteredClient(RegisteredClient registeredClient) {
|
||||
this.registeredClient = registeredClient;
|
||||
}
|
||||
|
||||
protected OAuth2Error getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
protected void setError(OAuth2Error error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
protected boolean hasError() {
|
||||
return getError() != null;
|
||||
}
|
||||
|
||||
protected boolean isRedirectOnError() {
|
||||
return this.redirectOnError;
|
||||
}
|
||||
|
||||
protected void setRedirectOnError(boolean redirectOnError) {
|
||||
this.redirectOnError = redirectOnError;
|
||||
}
|
||||
|
||||
protected abstract String resolveRedirectUri();
|
||||
}
|
||||
|
||||
private static class UserConsentPage {
|
||||
private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
|
||||
private static final String CONSENT_ACTION_PARAMETER_NAME = "consent_action";
|
||||
private static final String CONSENT_ACTION_APPROVE = "approve";
|
||||
private static final String CONSENT_ACTION_CANCEL = "cancel";
|
||||
|
||||
private static void displayConsent(HttpServletRequest request, HttpServletResponse response,
|
||||
RegisteredClient registeredClient, OAuth2Authorization authorization) throws IOException {
|
||||
|
||||
String consentPage = generateConsentPage(request, registeredClient, authorization);
|
||||
response.setContentType(TEXT_HTML_UTF8.toString());
|
||||
response.setContentLength(consentPage.getBytes(StandardCharsets.UTF_8).length);
|
||||
response.getWriter().write(consentPage);
|
||||
}
|
||||
|
||||
private static boolean isConsentApproved(HttpServletRequest request) {
|
||||
return CONSENT_ACTION_APPROVE.equalsIgnoreCase(request.getParameter(CONSENT_ACTION_PARAMETER_NAME));
|
||||
}
|
||||
|
||||
private static boolean isConsentCancelled(HttpServletRequest request) {
|
||||
return CONSENT_ACTION_CANCEL.equalsIgnoreCase(request.getParameter(CONSENT_ACTION_PARAMETER_NAME));
|
||||
}
|
||||
|
||||
private static String generateConsentPage(HttpServletRequest request,
|
||||
RegisteredClient registeredClient, OAuth2Authorization authorization) {
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
||||
OAuth2AuthorizationRequest.class.getName());
|
||||
Set<String> scopes = new HashSet<>(authorizationRequest.getScopes());
|
||||
scopes.remove(OidcScopes.OPENID); // openid scope does not require consent
|
||||
String state = authorization.getAttribute(
|
||||
OAuth2ParameterNames.STATE);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("<!DOCTYPE html>");
|
||||
builder.append("<html lang=\"en\">");
|
||||
builder.append("<head>");
|
||||
builder.append(" <meta charset=\"utf-8\">");
|
||||
builder.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">");
|
||||
builder.append(" <link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css\" integrity=\"sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z\" crossorigin=\"anonymous\">");
|
||||
builder.append(" <title>Consent required</title>");
|
||||
builder.append("</head>");
|
||||
builder.append("<body>");
|
||||
builder.append("<div class=\"container\">");
|
||||
builder.append(" <div class=\"py-5\">");
|
||||
builder.append(" <h1 class=\"text-center\">Consent required</h1>");
|
||||
builder.append(" </div>");
|
||||
builder.append(" <div class=\"row\">");
|
||||
builder.append(" <div class=\"col text-center\">");
|
||||
builder.append(" <p><span class=\"font-weight-bold text-primary\">" + registeredClient.getClientId() + "</span> wants to access your account <span class=\"font-weight-bold\">" + authorization.getPrincipalName() + "</span></p>");
|
||||
builder.append(" </div>");
|
||||
builder.append(" </div>");
|
||||
builder.append(" <div class=\"row pb-3\">");
|
||||
builder.append(" <div class=\"col text-center\">");
|
||||
builder.append(" <p>The following permissions are requested by the above app.<br/>Please review these and consent if you approve.</p>");
|
||||
builder.append(" </div>");
|
||||
builder.append(" </div>");
|
||||
builder.append(" <div class=\"row\">");
|
||||
builder.append(" <div class=\"col text-center\">");
|
||||
builder.append(" <form method=\"post\" action=\"" + request.getRequestURI() + "\">");
|
||||
builder.append(" <input type=\"hidden\" name=\"client_id\" value=\"" + registeredClient.getClientId() + "\">");
|
||||
builder.append(" <input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
|
||||
|
||||
for (String scope : scopes) {
|
||||
builder.append(" <div class=\"form-group form-check py-1\">");
|
||||
builder.append(" <input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" value=\"" + scope + "\" id=\"" + scope + "\" checked>");
|
||||
builder.append(" <label class=\"form-check-label\" for=\"" + scope + "\">" + scope + "</label>");
|
||||
builder.append(" </div>");
|
||||
}
|
||||
|
||||
builder.append(" <div class=\"form-group pt-3\">");
|
||||
builder.append(" <button class=\"btn btn-primary btn-lg\" type=\"submit\" name=\"consent_action\" value=\"approve\">Submit Consent</button>");
|
||||
builder.append(" </div>");
|
||||
builder.append(" <div class=\"form-group\">");
|
||||
builder.append(" <button class=\"btn btn-link regular\" type=\"submit\" name=\"consent_action\" value=\"cancel\">Cancel</button>");
|
||||
builder.append(" </div>");
|
||||
builder.append(" </form>");
|
||||
builder.append(" </div>");
|
||||
builder.append(" </div>");
|
||||
builder.append(" <div class=\"row pt-4\">");
|
||||
builder.append(" <div class=\"col text-center\">");
|
||||
builder.append(" <p><small>Your consent to provide access is required.<br/>If you do not approve, click Cancel, in which case no information will be shared with the app.</small></p>");
|
||||
builder.append(" </div>");
|
||||
builder.append(" </div>");
|
||||
builder.append("</div>");
|
||||
builder.append("</body>");
|
||||
builder.append("</html>");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import javax.servlet.ServletException;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A {@code Filter} that processes an authentication request for an OAuth 2.0 Client.
|
||||
|
@ -73,7 +74,11 @@ public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
|
|||
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.requestMatcher = requestMatcher;
|
||||
this.authenticationConverter = new ClientSecretBasicAuthenticationConverter();
|
||||
this.authenticationConverter = new DelegatingAuthenticationConverter(
|
||||
Arrays.asList(
|
||||
new ClientSecretBasicAuthenticationConverter(),
|
||||
new ClientSecretPostAuthenticationConverter(),
|
||||
new PublicClientAuthenticationConverter()));
|
||||
this.authenticationSuccessHandler = this::onAuthenticationSuccess;
|
||||
this.authenticationFailureHandler = this::onAuthenticationFailure;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.web;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
|
@ -46,4 +49,11 @@ final class OAuth2EndpointUtils {
|
|||
});
|
||||
return parameters;
|
||||
}
|
||||
|
||||
static boolean matchesPkceTokenRequest(HttpServletRequest request) {
|
||||
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
|
||||
request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) &&
|
||||
request.getParameter(OAuth2ParameterNames.CODE) != null &&
|
||||
request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,7 +15,21 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.web;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import java.io.IOException;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
|
@ -28,60 +42,55 @@ 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.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
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.OAuth2AuthorizationCodeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
|
||||
* which handles the processing of the OAuth 2.0 Access Token Request.
|
||||
* A {@code Filter} for the OAuth 2.0 Token endpoint,
|
||||
* which handles the processing of an OAuth 2.0 Authorization Grant.
|
||||
*
|
||||
* <p>
|
||||
* It converts the OAuth 2.0 Access Token Request to an {@link OAuth2AuthorizationCodeAuthenticationToken},
|
||||
* It converts the OAuth 2.0 Authorization Grant request to an {@link Authentication},
|
||||
* which is then authenticated by the {@link AuthenticationManager}.
|
||||
* If the authentication succeeds, the {@link AuthenticationManager} returns an
|
||||
* {@link OAuth2AccessTokenAuthenticationToken}, which contains
|
||||
* the {@link OAuth2AccessToken} that is returned in the response.
|
||||
* In case of any error, an {@link OAuth2Error} is returned in the response.
|
||||
* {@link OAuth2AccessTokenAuthenticationToken}, which is returned in the OAuth 2.0 Access Token response.
|
||||
* In case of any error, an {@link OAuth2Error} is returned in the OAuth 2.0 Error response.
|
||||
*
|
||||
* <p>
|
||||
* By default, this {@code Filter} responds to access token requests
|
||||
* By default, this {@code Filter} responds to authorization grant requests
|
||||
* at the {@code URI} {@code /oauth2/token} and {@code HttpMethod} {@code POST}.
|
||||
*
|
||||
* <p>
|
||||
* The default endpoint {@code URI} {@code /oauth2/token} may be overridden
|
||||
* via the constructor {@link #OAuth2TokenEndpointFilter(AuthenticationManager, OAuth2AuthorizationService, String)}.
|
||||
* via the constructor {@link #OAuth2TokenEndpointFilter(AuthenticationManager, String)}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Madhu Bhat
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.0.1
|
||||
* @see AuthenticationManager
|
||||
* @see OAuth2AuthorizationService
|
||||
* @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 OAuth2AuthorizationCodeAuthenticationProvider
|
||||
* @see OAuth2RefreshTokenAuthenticationProvider
|
||||
* @see OAuth2ClientCredentialsAuthenticationProvider
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.2">Section 3.2 Token Endpoint</a>
|
||||
*/
|
||||
public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
||||
/**
|
||||
|
@ -90,9 +99,8 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||
public static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final RequestMatcher tokenEndpointMatcher;
|
||||
private final Converter<HttpServletRequest, Authentication> authorizationGrantAuthenticationConverter;
|
||||
private final AuthenticationConverter authorizationGrantAuthenticationConverter;
|
||||
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
|
||||
new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
|
||||
|
@ -102,32 +110,27 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||
* Constructs an {@code OAuth2TokenEndpointFilter} using the provided parameters.
|
||||
*
|
||||
* @param authenticationManager the authentication manager
|
||||
* @param authorizationService the authorization service
|
||||
*/
|
||||
public OAuth2TokenEndpointFilter(AuthenticationManager authenticationManager,
|
||||
OAuth2AuthorizationService authorizationService) {
|
||||
this(authenticationManager, authorizationService, DEFAULT_TOKEN_ENDPOINT_URI);
|
||||
public OAuth2TokenEndpointFilter(AuthenticationManager authenticationManager) {
|
||||
this(authenticationManager, DEFAULT_TOKEN_ENDPOINT_URI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2TokenEndpointFilter} using the provided parameters.
|
||||
*
|
||||
* @param authenticationManager the authentication manager
|
||||
* @param authorizationService the authorization service
|
||||
* @param tokenEndpointUri the endpoint {@code URI} for access token requests
|
||||
*/
|
||||
public OAuth2TokenEndpointFilter(AuthenticationManager authenticationManager,
|
||||
OAuth2AuthorizationService authorizationService, String tokenEndpointUri) {
|
||||
public OAuth2TokenEndpointFilter(AuthenticationManager authenticationManager, String tokenEndpointUri) {
|
||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.hasText(tokenEndpointUri, "tokenEndpointUri cannot be empty");
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenEndpointMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name());
|
||||
Map<AuthorizationGrantType, Converter<HttpServletRequest, Authentication>> converters = new HashMap<>();
|
||||
converters.put(AuthorizationGrantType.AUTHORIZATION_CODE, new AuthorizationCodeAuthenticationConverter());
|
||||
converters.put(AuthorizationGrantType.CLIENT_CREDENTIALS, new ClientCredentialsAuthenticationConverter());
|
||||
this.authorizationGrantAuthenticationConverter = new DelegatingAuthorizationGrantAuthenticationConverter(converters);
|
||||
List<AuthenticationConverter> converters = new ArrayList<>();
|
||||
converters.add(new AuthorizationCodeAuthenticationConverter());
|
||||
converters.add(new RefreshTokenAuthenticationConverter());
|
||||
converters.add(new ClientCredentialsAuthenticationConverter());
|
||||
this.authorizationGrantAuthenticationConverter = new DelegatingAuthenticationConverter(converters);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -152,7 +155,7 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||
|
||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
|
||||
(OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);
|
||||
sendAccessTokenResponse(response, accessTokenAuthentication.getAccessToken());
|
||||
sendAccessTokenResponse(response, accessTokenAuthentication);
|
||||
|
||||
} catch (OAuth2AuthenticationException ex) {
|
||||
SecurityContextHolder.clearContext();
|
||||
|
@ -160,7 +163,13 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||
}
|
||||
}
|
||||
|
||||
private void sendAccessTokenResponse(HttpServletResponse response, OAuth2AccessToken accessToken) throws IOException {
|
||||
private void sendAccessTokenResponse(HttpServletResponse response,
|
||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication) throws IOException {
|
||||
|
||||
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
|
||||
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
|
||||
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
|
||||
|
||||
OAuth2AccessTokenResponse.Builder builder =
|
||||
OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
|
||||
.tokenType(accessToken.getTokenType())
|
||||
|
@ -168,6 +177,12 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
|
||||
builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
|
||||
}
|
||||
if (refreshToken != null) {
|
||||
builder.refreshToken(refreshToken.getTokenValue());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(additionalParameters)) {
|
||||
builder.additionalParameters(additionalParameters);
|
||||
}
|
||||
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
|
||||
|
@ -185,7 +200,7 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
private static class AuthorizationCodeAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
|
||||
private static class AuthorizationCodeAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
|
@ -195,18 +210,9 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||
return null;
|
||||
}
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
// client_id (REQUIRED)
|
||||
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
|
||||
Authentication clientPrincipal = null;
|
||||
if (StringUtils.hasText(clientId)) {
|
||||
if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
|
||||
}
|
||||
} else {
|
||||
clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
}
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
// code (REQUIRED)
|
||||
String code = parameters.getFirst(OAuth2ParameterNames.CODE);
|
||||
|
@ -223,13 +229,71 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
|
||||
}
|
||||
|
||||
return clientPrincipal != null ?
|
||||
new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri) :
|
||||
new OAuth2AuthorizationCodeAuthenticationToken(code, clientId, redirectUri);
|
||||
// @formatter:off
|
||||
Map<String, Object> additionalParameters = parameters
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.CODE) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
|
||||
// @formatter:on
|
||||
|
||||
return new OAuth2AuthorizationCodeAuthenticationToken(
|
||||
code, clientPrincipal, redirectUri, additionalParameters);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ClientCredentialsAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
|
||||
private static class RefreshTokenAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
// grant_type (REQUIRED)
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
// refresh_token (REQUIRED)
|
||||
String refreshToken = parameters.getFirst(OAuth2ParameterNames.REFRESH_TOKEN);
|
||||
if (!StringUtils.hasText(refreshToken) ||
|
||||
parameters.get(OAuth2ParameterNames.REFRESH_TOKEN).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REFRESH_TOKEN);
|
||||
}
|
||||
|
||||
// scope (OPTIONAL)
|
||||
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
|
||||
if (StringUtils.hasText(scope) &&
|
||||
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
|
||||
}
|
||||
Set<String> requestedScopes = null;
|
||||
if (StringUtils.hasText(scope)) {
|
||||
requestedScopes = new HashSet<>(
|
||||
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
Map<String, Object> additionalParameters = parameters
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.REFRESH_TOKEN) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.SCOPE))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
|
||||
// @formatter:on
|
||||
|
||||
return new OAuth2RefreshTokenAuthenticationToken(
|
||||
refreshToken, clientPrincipal, requestedScopes, additionalParameters);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ClientCredentialsAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
|
@ -249,13 +313,23 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
|||
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
|
||||
}
|
||||
Set<String> requestedScopes = null;
|
||||
if (StringUtils.hasText(scope)) {
|
||||
Set<String> requestedScopes = new HashSet<>(
|
||||
requestedScopes = new HashSet<>(
|
||||
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
|
||||
return new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal, requestedScopes);
|
||||
}
|
||||
|
||||
return new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal);
|
||||
// @formatter:off
|
||||
Map<String, Object> additionalParameters = parameters
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||
!e.getKey().equals(OAuth2ParameterNames.SCOPE))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
|
||||
// @formatter:on
|
||||
|
||||
return new OAuth2ClientCredentialsAuthenticationToken(
|
||||
clientPrincipal, requestedScopes, additionalParameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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.web;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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.OAuth2ParameterNames2;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@code Filter} for the OAuth 2.0 Token Revocation endpoint.
|
||||
*
|
||||
* @author Vivek Babu
|
||||
* @author Joe Grandja
|
||||
* @see OAuth2TokenRevocationAuthenticationProvider
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-2">Section 2 Token Revocation</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-2.1">Section 2.1 Revocation Request</a>
|
||||
* @since 0.0.3
|
||||
*/
|
||||
public class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFilter {
|
||||
/**
|
||||
* The default endpoint {@code URI} for token revocation requests.
|
||||
*/
|
||||
public static final String DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI = "/oauth2/revoke";
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final RequestMatcher tokenRevocationEndpointMatcher;
|
||||
private final Converter<HttpServletRequest, Authentication> tokenRevocationAuthenticationConverter =
|
||||
new DefaultTokenRevocationAuthenticationConverter();
|
||||
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
|
||||
new OAuth2ErrorHttpMessageConverter();
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2TokenRevocationEndpointFilter} using the provided parameters.
|
||||
*
|
||||
* @param authenticationManager the authentication manager
|
||||
*/
|
||||
public OAuth2TokenRevocationEndpointFilter(AuthenticationManager authenticationManager) {
|
||||
this(authenticationManager, DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2TokenRevocationEndpointFilter} using the provided parameters.
|
||||
*
|
||||
* @param authenticationManager the authentication manager
|
||||
* @param tokenRevocationEndpointUri the endpoint {@code URI} for token revocation requests
|
||||
*/
|
||||
public OAuth2TokenRevocationEndpointFilter(AuthenticationManager authenticationManager,
|
||||
String tokenRevocationEndpointUri) {
|
||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||
Assert.hasText(tokenRevocationEndpointUri, "tokenRevocationEndpointUri cannot be empty");
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.tokenRevocationEndpointMatcher = new AntPathRequestMatcher(
|
||||
tokenRevocationEndpointUri, HttpMethod.POST.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (!this.tokenRevocationEndpointMatcher.matches(request)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.authenticationManager.authenticate(
|
||||
this.tokenRevocationAuthenticationConverter.convert(request));
|
||||
response.setStatus(HttpStatus.OK.value());
|
||||
} catch (OAuth2AuthenticationException ex) {
|
||||
SecurityContextHolder.clearContext();
|
||||
sendErrorResponse(response, ex.getError());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
|
||||
this.errorHttpResponseConverter.write(error, null, httpResponse);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Revocation Parameter: " + parameterName,
|
||||
"https://tools.ietf.org/html/rfc7009#section-2.1");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
private static class DefaultTokenRevocationAuthenticationConverter
|
||||
implements Converter<HttpServletRequest, Authentication> {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
// token (REQUIRED)
|
||||
String token = parameters.getFirst(OAuth2ParameterNames2.TOKEN);
|
||||
if (!StringUtils.hasText(token) ||
|
||||
parameters.get(OAuth2ParameterNames2.TOKEN).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames2.TOKEN);
|
||||
}
|
||||
|
||||
// token_type_hint (OPTIONAL)
|
||||
String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames2.TOKEN_TYPE_HINT);
|
||||
if (StringUtils.hasText(tokenTypeHint) &&
|
||||
parameters.get(OAuth2ParameterNames2.TOKEN_TYPE_HINT).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames2.TOKEN_TYPE_HINT);
|
||||
}
|
||||
|
||||
return new OAuth2TokenRevocationAuthenticationToken(token, clientPrincipal, tokenTypeHint);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.web;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Attempts to extract the parameters from {@link HttpServletRequest}
|
||||
* used for authenticating public clients using Proof Key for Code Exchange (PKCE).
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.2
|
||||
* @see AuthenticationConverter
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see OAuth2ClientAuthenticationFilter
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636">Proof Key for Code Exchange by OAuth Public Clients</a>
|
||||
*/
|
||||
public class PublicClientAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
if (!OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
// client_id (REQUIRED for public clients)
|
||||
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
|
||||
if (!StringUtils.hasText(clientId) ||
|
||||
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
|
||||
}
|
||||
|
||||
// code_verifier (REQUIRED)
|
||||
if (parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
|
||||
}
|
||||
|
||||
parameters.remove(OAuth2ParameterNames.CLIENT_ID);
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(
|
||||
clientId, new HashMap<>(parameters.toSingleValueMap()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.config.annotation.web.configuration;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.OrderUtils;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2AuthorizationServerConfiguration}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class OAuth2AuthorizationServerConfigurationTests {
|
||||
|
||||
@Test
|
||||
public void assertOrderHighestPrecedence() {
|
||||
Method authorizationServerSecurityFilterChainMethod =
|
||||
ClassUtils.getMethod(
|
||||
OAuth2AuthorizationServerConfiguration.class,
|
||||
"authorizationServerSecurityFilterChain",
|
||||
HttpSecurity.class);
|
||||
Integer order = OrderUtils.getOrder(authorizationServerSecurityFilterChainMethod);
|
||||
assertThat(order).isEqualTo(Ordered.HIGHEST_PRECEDENCE);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
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.NimbusJwkSetEndpointFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Integration tests for the JWK Set endpoint.
|
||||
*
|
||||
* @author Florian Berthe
|
||||
*/
|
||||
public class JwkSetTests {
|
||||
private static RegisteredClientRepository registeredClientRepository;
|
||||
private static OAuth2AuthorizationService authorizationService;
|
||||
private static JWKSource<SecurityContext> jwkSource;
|
||||
private static ProviderSettings providerSettings;
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
providerSettings = new ProviderSettings().jwkSetEndpoint("/test/jwks");
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
reset(registeredClientRepository);
|
||||
reset(authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenJwkSetThenReturnKeys() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
assertJwkSetRequestThenReturnKeys(NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenJwkSetCustomEndpointThenReturnKeys() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire();
|
||||
|
||||
assertJwkSetRequestThenReturnKeys(providerSettings.jwkSetEndpoint());
|
||||
}
|
||||
|
||||
private void assertJwkSetRequestThenReturnKeys(String jwkSetEndpointUri) throws Exception {
|
||||
this.mvc.perform(get(jwkSetEndpointUri))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
|
||||
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
|
||||
.andExpect(jsonPath("$.keys").isNotEmpty())
|
||||
.andExpect(jsonPath("$.keys").isArray());
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
RegisteredClientRepository registeredClientRepository() {
|
||||
return registeredClientRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizationService authorizationService() {
|
||||
return authorizationService;
|
||||
}
|
||||
|
||||
@Bean
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
return jwkSource;
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfigurationCustomEndpoints extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
ProviderSettings providerSettings() {
|
||||
return providerSettings;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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,66 +15,106 @@
|
|||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.crypto.keys.KeyManager;
|
||||
import org.springframework.security.crypto.keys.StaticKeyGeneratingKeyManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.server.authorization.TokenType;
|
||||
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.client.TestRegisteredClients;
|
||||
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.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Integration tests for the OAuth 2.0 Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Daniel Garnier-Moiroux
|
||||
*/
|
||||
public class OAuth2AuthorizationCodeGrantTests {
|
||||
// See RFC 7636: Appendix B. Example for the S256 code_challenge_method
|
||||
// https://tools.ietf.org/html/rfc7636#appendix-B
|
||||
private static final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
|
||||
private static final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
|
||||
private static final String AUTHORITIES_CLAIM = "authorities";
|
||||
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
|
||||
|
||||
private static RegisteredClientRepository registeredClientRepository;
|
||||
private static OAuth2AuthorizationService authorizationService;
|
||||
private static KeyManager keyManager;
|
||||
private static JWKSource<SecurityContext> jwkSource;
|
||||
private static NimbusJwsEncoder jwtEncoder;
|
||||
private static NimbusJwtDecoder jwtDecoder;
|
||||
private static ProviderSettings providerSettings;
|
||||
private static HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
|
||||
new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
@ -86,7 +126,13 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|||
public static void init() {
|
||||
registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
keyManager = new StaticKeyGeneratingKeyManager();
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
jwtEncoder = new NimbusJwsEncoder(jwkSource);
|
||||
jwtDecoder = NimbusJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY).build();
|
||||
providerSettings = new ProviderSettings()
|
||||
.authorizationEndpoint("/test/authorize")
|
||||
.tokenEndpoint("/test/token");
|
||||
}
|
||||
|
||||
@Before
|
||||
|
@ -103,7 +149,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(MockMvcRequestBuilders.get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
MvcResult mvcResult = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.params(getAuthorizationRequestParameters(registeredClient)))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
@ -117,11 +163,22 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|||
public void requestWhenAuthorizationRequestAuthenticatedThenRedirectToClient() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
assertAuthorizationRequestRedirectsToClient(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenAuthorizationRequestCustomEndpointThenRedirectToClient() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire();
|
||||
|
||||
assertAuthorizationRequestRedirectsToClient(providerSettings.authorizationEndpoint());
|
||||
}
|
||||
|
||||
private void assertAuthorizationRequestRedirectsToClient(String authorizationEndpointUri) throws Exception {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
MvcResult mvcResult = this.mvc.perform(get(authorizationEndpointUri)
|
||||
.params(getAuthorizationRequestParameters(registeredClient))
|
||||
.with(user("user")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
|
@ -133,7 +190,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenTokenRequestValidThenResponseIncludesCacheHeaders() throws Exception {
|
||||
public void requestWhenTokenRequestValidThenReturnAccessTokenResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
|
@ -142,23 +199,134 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
|
||||
when(authorizationService.findByToken(
|
||||
eq(authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE)),
|
||||
eq(TokenType.AUTHORIZATION_CODE)))
|
||||
eq(authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue()),
|
||||
eq(AUTHORIZATION_CODE_TOKEN_TYPE)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
this.mvc.perform(MockMvcRequestBuilders.post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
OAuth2AccessTokenResponse accessTokenResponse = assertTokenRequestReturnsAccessTokenResponse(
|
||||
registeredClient, authorization, OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI);
|
||||
|
||||
// Assert user authorities was propagated as claim in JWT
|
||||
Jwt jwt = jwtDecoder.decode(accessTokenResponse.getAccessToken().getTokenValue());
|
||||
List<String> authoritiesClaim = jwt.getClaim(AUTHORITIES_CLAIM);
|
||||
Authentication principal = authorization.getAttribute(Principal.class.getName());
|
||||
Set<String> userAuthorities = principal.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toSet());
|
||||
assertThat(authoritiesClaim).containsExactlyInAnyOrderElementsOf(userAuthorities);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenTokenRequestCustomEndpointThenReturnAccessTokenResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
|
||||
when(authorizationService.findByToken(
|
||||
eq(authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue()),
|
||||
eq(AUTHORIZATION_CODE_TOKEN_TYPE)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
assertTokenRequestReturnsAccessTokenResponse(
|
||||
registeredClient, authorization, providerSettings.tokenEndpoint());
|
||||
}
|
||||
|
||||
private OAuth2AccessTokenResponse assertTokenRequestReturnsAccessTokenResponse(RegisteredClient registeredClient,
|
||||
OAuth2Authorization authorization, String tokenEndpointUri) throws Exception {
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(post(tokenEndpointUri)
|
||||
.params(getTokenRequestParameters(registeredClient, authorization))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
|
||||
registeredClient.getClientId(), registeredClient.getClientSecret())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
|
||||
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")));
|
||||
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.token_type").isNotEmpty())
|
||||
.andExpect(jsonPath("$.expires_in").isNotEmpty())
|
||||
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.scope").isNotEmpty())
|
||||
.andReturn();
|
||||
|
||||
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
|
||||
verify(authorizationService).findByToken(
|
||||
eq(authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE)),
|
||||
eq(TokenType.AUTHORIZATION_CODE));
|
||||
eq(authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue()),
|
||||
eq(AUTHORIZATION_CODE_TOKEN_TYPE));
|
||||
verify(authorizationService).save(any());
|
||||
|
||||
MockHttpServletResponse servletResponse = mvcResult.getResponse();
|
||||
MockClientHttpResponse httpResponse = new MockClientHttpResponse(
|
||||
servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
|
||||
return accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenPublicClientWithPkceThenReturnAccessTokenResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.params(getAuthorizationRequestParameters(registeredClient))
|
||||
.param(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
|
||||
.param(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
|
||||
.with(user("user")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
|
||||
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
|
||||
|
||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
||||
verify(authorizationService).save(authorizationCaptor.capture());
|
||||
OAuth2Authorization authorization = authorizationCaptor.getValue();
|
||||
|
||||
when(authorizationService.findByToken(
|
||||
eq(authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue()),
|
||||
eq(AUTHORIZATION_CODE_TOKEN_TYPE)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.params(getTokenRequestParameters(registeredClient, authorization))
|
||||
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||
.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.token_type").isNotEmpty())
|
||||
.andExpect(jsonPath("$.expires_in").isNotEmpty())
|
||||
.andExpect(jsonPath("$.refresh_token").doesNotExist())
|
||||
.andExpect(jsonPath("$.scope").isNotEmpty());
|
||||
|
||||
verify(registeredClientRepository, times(2)).findByClientId(eq(registeredClient.getClientId()));
|
||||
verify(authorizationService, times(2)).findByToken(
|
||||
eq(authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue()),
|
||||
eq(AUTHORIZATION_CODE_TOKEN_TYPE));
|
||||
verify(authorizationService, times(2)).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenCustomJwtEncoderThenUsed() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigurationWithJwtEncoder.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
|
||||
when(authorizationService.findByToken(
|
||||
eq(authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue()),
|
||||
eq(AUTHORIZATION_CODE_TOKEN_TYPE)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.params(getTokenRequestParameters(registeredClient, authorization))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
|
||||
registeredClient.getClientId(), registeredClient.getClientSecret())));
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
|
||||
|
@ -176,7 +344,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|||
OAuth2Authorization authorization) {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
parameters.set(OAuth2ParameterNames.CODE, authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE));
|
||||
parameters.set(OAuth2ParameterNames.CODE, authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue());
|
||||
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
|
||||
return parameters;
|
||||
}
|
||||
|
@ -204,8 +372,43 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|||
}
|
||||
|
||||
@Bean
|
||||
KeyManager keyManager() {
|
||||
return keyManager;
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
return jwkSource;
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
|
||||
return context -> {
|
||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getAuthorizationGrantType()) &&
|
||||
OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
|
||||
Authentication principal = context.getPrincipal();
|
||||
Set<String> authorities = principal.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toSet());
|
||||
context.getClaims().claim(AUTHORITIES_CLAIM, authorities);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfigurationWithJwtEncoder extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
JwtEncoder jwtEncoder() {
|
||||
return jwtEncoder;
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfigurationCustomEndpoints extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
ProviderSettings providerSettings() {
|
||||
return providerSettings;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user