add reactive/webflux/java folder

This commit is contained in:
Rob Winch
2020-07-29 14:51:50 -05:00
parent 56beccdef9
commit 1077d2c75c
138 changed files with 9 additions and 9 deletions
@@ -0,0 +1,324 @@
NOTE: Spring Security Reactive OAuth only supports authentication using a user info endpoint.
Support for JWT validation will be added in https://github.com/spring-projects/spring-security/issues/5330[gh-5330].
= OAuth 2.0 Login Sample
This guide provides instructions on setting up the sample application with OAuth 2.0 Login using an OAuth 2.0 Provider or OpenID Connect 1.0 Provider.
The sample application uses Spring Boot 2.0.0.M6 and the `spring-security-oauth2-client` module which is new in Spring Security 5.0.
The following sections provide detailed steps for setting up OAuth 2.0 Login for these Providers:
* <<google-login, Google>>
* <<github-login, GitHub>>
* <<facebook-login, Facebook>>
* <<okta-login, Okta>>
[[google-login]]
== Login with Google
This section shows how to configure the sample application using Google as the Authentication Provider and covers the following topics:
* <<google-initial-setup,Initial setup>>
* <<google-redirect-uri,Setting the redirect URI>>
* <<google-application-config,Configure application.yml>>
* <<google-boot-application,Boot up the application>>
[[google-initial-setup]]
=== Initial setup
To use Google's OAuth 2.0 authentication system for login, you must set up a project in the Google API Console to obtain OAuth 2.0 credentials.
NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the
https://openid.net/connect/[OpenID Connect 1.0] specification and is https://openid.net/certification/[OpenID Certified].
Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page, starting in the section, "Setting up OAuth 2.0".
After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret.
[[google-redirect-uri]]
=== Setting the redirect URI
The redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Google
and have granted access to the OAuth Client _(created in the previous step)_ on the Consent page.
In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`.
TIP: The default redirect URI template is `{baseUrl}/login/oauth2/code/{registrationId}`.
The *_registrationId_* is a unique identifier for the `ClientRegistration`.
IMPORTANT: If the application is running behind a proxy server, it is recommended to check https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#appendix-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured.
Also, see the supported https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2Client-auth-code-redirect-uri[`URI` template variables] for `redirect-uri`.
[[google-application-config]]
=== Configure application.yml
Now that you have a new OAuth Client with Google, you need to configure the application to use the OAuth Client for the _authentication flow_. To do so:
. Go to `application.yml` and set the following configuration:
+
[source,yaml]
----
spring:
security:
oauth2:
client:
registration: <1>
google: <2>
client-id: google-client-id
client-secret: google-client-secret
----
+
.OAuth Client properties
====
<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties.
<2> Following the base property prefix is the ID for the `ClientRegistration`, such as google.
====
. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier.
[[google-boot-application]]
=== Boot up the application
Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`.
You are then redirected to the default _auto-generated_ login page, which displays a link for Google.
Click on the Google link, and you are then redirected to Google for authentication.
After authenticating with your Google account credentials, the next page presented to you is the Consent screen.
The Consent screen asks you to either allow or deny access to the OAuth Client you created earlier.
Click *Allow* to authorize the OAuth Client to access your email address and basic profile information.
At this point, the OAuth Client retrieves your email address and basic profile information
from the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] and establishes an authenticated session.
[[github-login]]
== Login with GitHub
This section shows how to configure the sample application using GitHub as the Authentication Provider and covers the following topics:
* <<github-register-application,Register OAuth application>>
* <<github-application-config,Configure application.yml>>
* <<github-boot-application,Boot up the application>>
[[github-register-application]]
=== Register OAuth application
To use GitHub's OAuth 2.0 authentication system for login, you must https://github.com/settings/applications/new[Register a new OAuth application].
When registering the OAuth application, ensure the *Authorization callback URL* is set to `http://localhost:8080/login/oauth2/code/github`.
The Authorization callback URL (redirect URI) is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with GitHub
and have granted access to the OAuth application on the _Authorize application_ page.
TIP: The default redirect URI template is `{baseUrl}/login/oauth2/code/{registrationId}`.
The *_registrationId_* is a unique identifier for the `ClientRegistration`.
IMPORTANT: If the application is running behind a proxy server, it is recommended to check https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#appendix-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured.
Also, see the supported https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2Client-auth-code-redirect-uri[`URI` template variables] for `redirect-uri`.
[[github-application-config]]
=== Configure application.yml
Now that you have a new OAuth application with GitHub, you need to configure the application to use the OAuth application for the _authentication flow_. To do so:
. Go to `application.yml` and set the following configuration:
+
[source,yaml]
----
spring:
security:
oauth2:
client:
registration: <1>
github: <2>
client-id: github-client-id
client-secret: github-client-secret
----
+
.OAuth Client properties
====
<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties.
<2> Following the base property prefix is the ID for the `ClientRegistration`, such as github.
====
. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier.
[[github-boot-application]]
=== Boot up the application
Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`.
You are then redirected to the default _auto-generated_ login page, which displays a link for GitHub.
Click on the GitHub link, and you are then redirected to GitHub for authentication.
After authenticating with your GitHub credentials, the next page presented to you is "Authorize application".
This page will ask you to *Authorize* the application you created in the previous step.
Click _Authorize application_ to allow the OAuth application to access your personal user data information.
At this point, the OAuth Client retrieves your personal user information
from the UserInfo Endpoint and establishes an authenticated session.
[TIP]
For detailed information returned from the UserInfo Endpoint, see the API documentation
for https://developer.github.com/v3/users/#get-the-authenticated-user["Get the authenticated user"].
[[facebook-login]]
== Login with Facebook
This section shows how to configure the sample application using Facebook as the Authentication Provider and covers the following topics:
* <<facebook-register-application,Add a New App>>
* <<facebook-application-config,Configure application.yml>>
* <<facebook-boot-application,Boot up the application>>
[[facebook-register-application]]
=== Add a New App
To use Facebook's OAuth 2.0 authentication system for login, you must first https://developers.facebook.com/apps[Add a New App].
Select "Create a New App" and then the "Create a New App ID" page is presented. Enter the Display Name, Contact Email, Category and then click "Create App ID".
NOTE: The selection for the _Category_ field is not relevant but it's a required field - select "Local".
The next page presented is "Product Setup". Click the "Get Started" button for the *Facebook Login* product.
In the left sidebar, under _Products -> Facebook Login_, select _Settings_.
For the field *Valid OAuth redirect URIs*, enter `http://localhost:8080/login/oauth2/code/facebook` then click _Save Changes_.
The OAuth redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Facebook
and have granted access to the application on the _Authorize application_ page.
TIP: The default redirect URI template is `{baseUrl}/login/oauth2/code/{registrationId}`.
The *_registrationId_* is a unique identifier for the `ClientRegistration`.
IMPORTANT: If the application is running behind a proxy server, it is recommended to check https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#appendix-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured.
Also, see the supported https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2Client-auth-code-redirect-uri[`URI` template variables] for `redirect-uri`.
[[facebook-application-config]]
=== Configure application.yml
Now that you have created a new application with Facebook, you need to configure the sample application to use the application for the _authentication flow_. To do so:
. Go to `application.yml` and set the following configuration:
+
[source,yaml]
----
spring:
security:
oauth2:
client:
registration: <1>
facebook: <2>
client-id: facebook-client-id
client-secret: facebook-client-secret
----
+
.OAuth Client properties
====
<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties.
<2> Following the base property prefix is the ID for the `ClientRegistration`, such as facebook.
====
. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier.
[[facebook-boot-application]]
=== Boot up the application
Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`.
You are then redirected to the default _auto-generated_ login page, which displays a link for Facebook.
Click on the Facebook link, and you are then redirected to Facebook for authentication.
After authenticating with your Facebook credentials, the next page presented to you is "Authorize application".
This page will ask you to *Authorize* the application you created in the previous step.
Click _Authorize application_ to allow the OAuth application to access your _public profile_ and _email address_ information.
At this point, the OAuth Client retrieves your personal user information
from the UserInfo Endpoint and establishes an authenticated session.
[[okta-login]]
== Login with Okta
This section shows how to configure the sample application using Okta as the Authentication Provider and covers the following topics:
* <<okta-register-application,Add Application>>
* <<okta-assign-application-people,Assign Application to People>>
* <<okta-application-config,Configure application.yml>>
* <<okta-boot-application,Boot up the application>>
[[okta-register-application]]
=== Add Application
To use Okta's OAuth 2.0 authentication system for login, you must first https://www.okta.com/developer/signup[create a developer account].
Sign in to your account sub-domain and navigate to _Applications -> Applications_ and then select the "Add Application" button.
From the "Add Application" page, select the "Create New App" button and enter the following:
* *Platform:* Web
* *Sign on method:* OpenID Connect
Select the _Create_ button.
On the "General Settings" page, enter the Application Name (for example, "Spring Security Okta Login") and then select the _Next_ button.
On the "Configure OpenID Connect" page, enter `http://localhost:8080/login/oauth2/code/okta` for the field *Redirect URIs* and then select _Finish_.
The redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Okta
and have granted access to the application on the _Authorize application_ page.
TIP: The default redirect URI template is `{baseUrl}/login/oauth2/code/{registrationId}`.
The *_registrationId_* is a unique identifier for the `ClientRegistration`.
IMPORTANT: If the application is running behind a proxy server, it is recommended to check https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#appendix-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured.
Also, see the supported https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2Client-auth-code-redirect-uri[`URI` template variables] for `redirect-uri`.
[[okta-assign-application-people]]
=== Assign Application to People
From the "General" tab of the application, select the "Assignments" tab and then select the _Assign_ button.
Select _Assign to People_ and assign your account to the application. Then select the _Save and Go Back_ button.
[[okta-application-config]]
=== Configure application.yml
Now that you have created a new application with Okta, you need to configure the sample application to use the application for the _authentication flow_. To do so:
. Go to `application.yml` and set the following configuration:
+
[source,yaml]
----
spring:
security:
oauth2:
client:
registration: <1>
okta: <2>
client-id: okta-client-id
client-secret: okta-client-secret
provider: <3>
okta:
authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
user-name-attribute: sub
jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys
----
+
.OAuth Client properties
====
<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties.
<2> Following the base property prefix is the ID for the `ClientRegistration`, such as okta.
<3> `spring.security.oauth2.client.provider` is the base property prefix for OAuth Provider properties.
====
. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier.
As well, replace `https://your-subdomain.oktapreview.com` in `authorization-uri`, `token-uri`, `user-info-uri` and `jwk-set-uri` with the sub-domain assigned to your account during the registration process.
[[okta-boot-application]]
=== Boot up the application
Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`.
You are then redirected to the default _auto-generated_ login page, which displays a link for Okta.
Click on the Okta link, and you are then redirected to Okta for authentication.
After authenticating with your Okta account credentials, the OAuth Client retrieves your email address and basic profile information
from the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] and establishes an authenticated session.
@@ -0,0 +1,27 @@
plugins {
id 'org.springframework.boot' version '2.2.6.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id "nebula.integtest" version "7.0.9"
id 'java'
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
integTestImplementation 'net.sourceforge.htmlunit:htmlunit'
}
tasks.withType(Test).configureEach {
useJUnitPlatform()
}
@@ -0,0 +1 @@
spring-security.version=5.4.0.BUILD-SNAPSHOT
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author 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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+104
View File
@@ -0,0 +1,104 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1 @@
@@ -0,0 +1,73 @@
/*
* 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 example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.hamcrest.CoreMatchers.containsString;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Login;
/**
* Tests for {@link ReactiveOAuth2LoginApplication}.
*
* @author Rob Winch
*/
@SpringBootTest
@AutoConfigureWebTestClient
public class OAuth2LoginApplicationTests {
@Autowired
WebTestClient test;
@Autowired
ReactiveClientRegistrationRepository clientRegistrationRepository;
@Test
void requestWhenMockOidcLoginThenIndex() {
// @formatter:off
this.clientRegistrationRepository.findByRegistrationId("github")
.map((clientRegistration) ->
this.test.mutateWith(mockOAuth2Login().clientRegistration(clientRegistration))
.get()
.uri("/")
.exchange()
.expectBody(String.class).value(containsString("GitHub"))
)
.block();
// @formatter:on
}
@TestConfiguration
static class AuthorizedClient {
@Bean
ServerOAuth2AuthorizedClientRepository authorizedClientRepository() {
return new WebSessionServerOAuth2AuthorizedClientRepository();
}
}
}
@@ -0,0 +1,44 @@
/*
* 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 example;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
/**
* Index controller.
*
* @author Rob Winch
*/
@Controller
public class OAuth2LoginController {
@GetMapping("/")
public String index(Model model, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,
@AuthenticationPrincipal OAuth2User oauth2User) {
model.addAttribute("userName", oauth2User.getName());
model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName());
model.addAttribute("userAttributes", oauth2User.getAttributes());
return "index";
}
}
@@ -0,0 +1,33 @@
/*
* 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 example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* A reactive OAuth2 Log in application.
*
* @author Rob Winch
*/
@SpringBootApplication
public class ReactiveOAuth2LoginApplication {
public static void main(String[] args) {
SpringApplication.run(ReactiveOAuth2LoginApplication.class, args);
}
}
@@ -0,0 +1,35 @@
server:
port: 8080
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
# org.springframework.boot.autoconfigure: DEBUG
spring:
thymeleaf:
cache: false
security:
oauth2:
client:
registration:
google:
client-id: your-app-client-id
client-secret: your-app-client-secret
github:
client-id: your-app-client-id
client-secret: your-app-client-secret
facebook:
client-id: your-app-client-id
client-secret: your-app-client-secret
okta:
client-id: your-app-client-id
client-secret: your-app-client-secret
provider:
okta:
authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<!--
~ /*
~ * 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.
~ */
~
-->
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Spring Security - OAuth 2.0 Login</title>
<meta charset="utf-8" />
</head>
<body>
<div style="float: right" th:fragment="logout">
<div style="float:left">
<span style="font-weight:bold">User: </span><span th:text="${userName}"></span>
</div>
<div style="float:none">&nbsp;</div>
<div style="float:right">
<a th:href="@{/logout}">Log Out</a>
</div>
</div>
<h1>OAuth 2.0 Login with Spring Security</h1>
<div>
You are successfully logged in <span style="font-weight:bold" th:text="${userName}"></span>
via the OAuth 2.0 Client <span style="font-weight:bold" th:text="${clientName}"></span>
</div>
<div>&nbsp;</div>
<div>
<span style="font-weight:bold">User Attributes:</span>
<ul>
<li th:each="userAttribute : ${userAttributes}">
<span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span>
</li>
</ul>
</div>
</body>
</html>
@@ -0,0 +1,85 @@
/*
* 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 example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.result.view.ViewResolver;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Login;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
/**
* @author Josh Cummings
*/
@WebFluxTest(OAuth2LoginController.class)
public class OAuth2LoginControllerTests {
@Autowired
OAuth2LoginController controller;
@Autowired
ViewResolver viewResolver;
@Mock
ReactiveClientRegistrationRepository clientRegistrationRepository;
WebTestClient rest;
@BeforeEach
void setup() {
ServerOAuth2AuthorizedClientRepository authorizedClientRepository = new WebSessionServerOAuth2AuthorizedClientRepository();
// @formatter:off
this.rest = WebTestClient
.bindToController(this.controller)
.apply(springSecurity())
.webFilter(new SecurityContextServerWebExchangeWebFilter())
.argumentResolvers((c) -> {
c.addCustomResolver(new AuthenticationPrincipalArgumentResolver(new ReactiveAdapterRegistry()));
c.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(this.clientRegistrationRepository, authorizedClientRepository));
})
.viewResolvers((c) -> c.viewResolver(this.viewResolver))
.build();
// @formatter:on
}
@Test
void indexGreetsAuthenticatedUser() {
// @formatter:off
this.rest.mutateWith(mockOAuth2Login())
.get()
.uri("/")
.exchange()
.expectBody(String.class).value(containsString("user"));
// @formatter:on
}
}
@@ -0,0 +1,112 @@
= OAuth 2.0 Resource Server Sample
This sample demonstrates integrating Resource Server with a mock Authorization Server, though it can be modified to integrate
with your favorite Authorization Server.
With it, you can run the integration tests or run the application as a stand-alone service to explore how you can
secure your own service with OAuth 2.0 Bearer Tokens using Spring Security.
== 1. Running the tests
To run the tests, do:
```bash
./gradlew integrationTest
```
Or import the project into your IDE and run `ServerOAuth2ResourceServerApplicationTests` from there.
=== What is it doing?
By default, the tests are pointing at a mock Authorization Server instance.
The tests are configured with a set of hard-coded tokens originally obtained from the mock Authorization Server,
and each makes a query to the Resource Server with their corresponding token.
The Resource Server subsquently verifies with the Authorization Server and authorizes the request, returning the phrase
```bash
Hello, subject!
```
where "subject" is the value of the `sub` field in the JWT returned by the Authorization Server.
== 2. Running the app
To run as a stand-alone application, do:
```bash
./gradlew bootRun
```
Or import the project into your IDE and run `ServerOAuth2ResourceServerApplication` from there.
Once it is up, you can use the following token:
```bash
export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww
```
And then make this request:
```bash
curl -H "Authorization: Bearer $TOKEN" localhost:8080
```
Which will respond with the phrase:
```bash
Hello, subject!
```
where `subject` is the value of the `sub` field in the JWT returned by the Authorization Server.
Or this:
```bash
export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A
curl -H "Authorization: Bearer $TOKEN" localhost:8080/message
```
Will respond with:
```bash
secret message
```
== 2. Testing against other Authorization Servers
_In order to use this sample, your Authorization Server must support JWTs that either use the "scope" or "scp" attribute._
To change the sample to point at your Authorization Server, simply find this property in the `application.yml`:
```yaml
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json
```
And change the property to your Authorization Server's JWK set endpoint:
```yaml
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://dev-123456.oktapreview.com/oauth2/default/v1/keys
```
And then you can run the app the same as before:
```bash
./gradlew bootRun
```
Make sure to obtain valid tokens from your Authorization Server in order to play with the sample Resource Server.
To use the `/` endpoint, any valid token from your Authorization Server will do.
To use the `/message` endpoint, the token should have the `message:read` scope.
@@ -0,0 +1,24 @@
plugins {
id 'org.springframework.boot' version '2.3.1.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id "nebula.integtest" version "7.0.9"
id 'java'
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
}
dependencies {
implementation 'com.squareup.okhttp3:mockwebserver'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.withType(Test).configureEach {
useJUnitPlatform()
}
@@ -0,0 +1 @@
spring-security.version=5.4.0.BUILD-SNAPSHOT
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author 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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+104
View File
@@ -0,0 +1,104 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1 @@
@@ -0,0 +1,85 @@
/*
* 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 example;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.hamcrest.Matchers.containsString;
/**
* Integration tests for resource server.
*
* @author Rob Winch
* @since 5.1
*/
@SpringBootTest
@AutoConfigureWebTestClient
public class ServerOAuth2ResourceServerApplicationITests {
Consumer<HttpHeaders> noScopesToken = (http) -> http.setBearerAuth(
"eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww");
Consumer<HttpHeaders> messageReadToken = (http) -> http.setBearerAuth(
"eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A");
@Autowired
private WebTestClient rest;
@Test
void getWhenValidBearerTokenThenAllows() {
// @formatter:off
this.rest.get().uri("/")
.headers(this.noScopesToken)
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello, subject!");
// @formatter:on
}
// -- tests with scopes
@Test
void getWhenValidBearerTokenThenScopedRequestsAlsoWork() {
// @formatter:off
this.rest.get().uri("/message")
.headers(this.messageReadToken)
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("secret message");
// @formatter:on
}
@Test
void getWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess() {
// @formatter:off
this.rest.get().uri("/message")
.headers(this.noScopesToken)
.exchange()
.expectStatus().isForbidden()
.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, containsString("Bearer error=\"insufficient_scope\""));
// @formatter:on
}
}
@@ -0,0 +1,48 @@
/*
* 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 example;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* Message controller.
*
* @author Josh Cummings
*/
@RestController
public class OAuth2ResourceServerController {
@GetMapping("/")
public String index(@AuthenticationPrincipal Jwt jwt) {
return String.format("Hello, %s!", jwt.getSubject());
}
@GetMapping("/message")
public String message() {
return "secret message";
}
@PostMapping("/message")
public String createMessage(@RequestBody String message) {
return String.format("Message was created. Content: %s", message);
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* Basic security resource server.
*
* @author Rob Winch
* @since 5.1
*/
@EnableWebFluxSecurity
public class SecurityConfiguration {
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((exchanges) -> exchanges
.pathMatchers(HttpMethod.GET, "/message/**").hasAuthority("SCOPE_message:read")
.pathMatchers(HttpMethod.POST, "/message/**").hasAuthority("SCOPE_message:write")
.anyExchange().authenticated()
)
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(withDefaults())
);
// @formatter:on
return http.build();
}
}
@@ -0,0 +1,33 @@
/*
* 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 example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Resource Server application.
*
* @author Rob Winch
*/
@SpringBootApplication
public class ServerOAuth2ResourceServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerOAuth2ResourceServerApplication.class, args);
}
}
@@ -0,0 +1,42 @@
/*
* 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.boot.env;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.SpringApplication;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* Adds {@link MockWebServerPropertySource} to the environment.
*
* @author Rob Winch
*/
public class MockWebServerEnvironmentPostProcessor implements EnvironmentPostProcessor, DisposableBean {
private final MockWebServerPropertySource propertySource = new MockWebServerPropertySource();
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
environment.getPropertySources().addFirst(this.propertySource);
}
@Override
public void destroy() throws Exception {
this.propertySource.destroy();
}
}
@@ -0,0 +1,124 @@
/*
* 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.boot.env;
import java.io.IOException;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.env.PropertySource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
/**
* Adds value for mockwebserver.url property.
*
* @author Rob Winch
*/
public class MockWebServerPropertySource extends PropertySource<MockWebServer> implements DisposableBean {
private static final MockResponse JWKS_RESPONSE = response(
"{\"keys\":[{\"p\":\"2p-ViY7DE9ZrdWQb544m0Jp7Cv03YCSljqfim9pD4ALhObX0OrAznOiowTjwBky9JGffMwDBVSfJSD9TSU7aH2sbbfi0bZLMdekKAuimudXwUqPDxrrg0BCyvCYgLmKjbVT3zcdylWSog93CNTxGDPzauu-oc0XPNKCXnaDpNvE\",\"kty\":\"RSA\",\"q\":\"sP_QYavrpBvSJ86uoKVGj2AGl78CSsAtpf1ybSY5TwUlorXSdqapRbY69Y271b0aMLzlleUn9ZTBO1dlKV2_dw_lPADHVia8z3pxL-8sUhIXLsgj4acchMk4c9YX-sFh07xENnyZ-_TXm3llPLuL67HUfBC2eKe800TmCYVWc9U\",\"d\":\"bn1nFxCQT4KLTHqo8mo9HvHD0cRNRNdWcKNnnEQkCF6tKbt-ILRyQGP8O40axLd7CoNVG9c9p_-g4-2kwCtLJNv_STLtwfpCY7VN5o6-ZIpfTjiW6duoPrLWq64Hm_4LOBQTiZfUPcLhsuJRHbWqakj-kV_YbUyC2Ocf_dd8IAQcSrAU2SCcDebhDCWwRUFvaa9V5eq0851S9goaA-AJz-JXyePH6ZFr8JxmWkWxYZ5kdcMD-sm9ZbxE0CaEk32l4fE4hR-L8x2dDtjWA-ahKCZ091z-gV3HWtR2JOjvxoNRjxUo3UxaGiFJHWNIl0EYUJZu1Cb-5wIlEI7wPx5mwQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"qS0OK48M2CIAA6_4Wdw4EbCaAfcTLf5Oy9t5BOF_PFUKqoSpZ6JsT5H0a_4zkjt-oI969v78OTlvBKbmEyKO-KeytzHBAA5CsLmVcz0THrMSg6oXZqu66MPnvWoZN9FEN5TklPOvBFm8Bg1QZ3k-YMVaM--DLvhaYR95_mqaz50\",\"dp\":\"Too2NozLGD1XrXyhabZvy1E0EuaVFj0UHQPDLSpkZ_2g3BK6Art6T0xmE8RYtmqrKIEIdlI3IliAvyvAx_1D7zWTTRaj-xlZyqJFrnXWL7zj8UxT8PkB-r2E-ILZ3NAi1gxIWezlBTZ8M6NfObDFmbTc_3tJkN_raISo8z_ziIE\",\"dq\":\"U0yhSkY5yOsa9YcMoigGVBWSJLpNHtbg5NypjHrPv8OhWbkOSq7WvSstBkFk5AtyFvvfZLMLIkWWxxGzV0t6f1MoxBtttLrYYyCxwihiiGFhLbAdSuZ1wnxcqA9bC7UVECvrQmVTpsMs8UupfHKbQBpZ8OWAqrnuYNNtG4_4Bt0\",\"n\":\"lygtuZj0lJjqOqIWocF8Bb583QDdq-aaFg8PesOp2-EDda6GqCpL-_NZVOflNGX7XIgjsWHcPsQHsV9gWuOzSJ0iEuWvtQ6eGBP5M6m7pccLNZfwUse8Cb4Ngx3XiTlyuqM7pv0LPyppZusfEHVEdeelou7Dy9k0OQ_nJTI3b2E1WBoHC58CJ453lo4gcBm1efURN3LIVc1V9NQY_ESBKVdwqYyoJPEanURLVGRd6cQKn6YrCbbIRHjqAyqOE-z3KmgDJnPriljfR5XhSGyM9eqD9Xpy6zu_MAeMJJfSArp857zLPk-Wf5VP9STAcjyfdBIybMKnwBYr2qHMT675hQ\"}]}",
200);
private static final MockResponse NOT_FOUND_RESPONSE = response(
"{ \"message\" : \"This mock authorization server responds to just one request: GET /.well-known/jwks.json.\" }",
404);
/**
* Name of the random {@link PropertySource}.
*/
public static final String MOCK_WEB_SERVER_PROPERTY_SOURCE_NAME = "mockwebserver";
private static final String NAME = "mockwebserver.url";
private static final Log logger = LogFactory.getLog(MockWebServerPropertySource.class);
private boolean started;
public MockWebServerPropertySource() {
super(MOCK_WEB_SERVER_PROPERTY_SOURCE_NAME, new MockWebServer());
}
@Override
public Object getProperty(String name) {
if (!name.equals(NAME)) {
return null;
}
if (logger.isTraceEnabled()) {
logger.trace("Looking up the url for '" + name + "'");
}
String url = getUrl();
return url;
}
@Override
public void destroy() throws Exception {
getSource().shutdown();
}
/**
* Get's the URL (i.e. "http://localhost:123456")
* @return the url to use
*/
private String getUrl() {
MockWebServer mockWebServer = getSource();
if (!this.started) {
intializeMockWebServer(mockWebServer);
}
String url = mockWebServer.url("").url().toExternalForm();
return url.substring(0, url.length() - 1);
}
private void intializeMockWebServer(MockWebServer mockWebServer) {
Dispatcher dispatcher = new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
if ("/.well-known/jwks.json".equals(request.getPath())) {
return JWKS_RESPONSE;
}
return NOT_FOUND_RESPONSE;
}
};
mockWebServer.setDispatcher(dispatcher);
try {
mockWebServer.start();
this.started = true;
}
catch (IOException ex) {
throw new RuntimeException("Could not start " + mockWebServer, ex);
}
}
private static MockResponse response(String body, int status) {
// @formatter:off
return new MockResponse()
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setResponseCode(status)
.setBody(body);
// @formatter:on
}
}
@@ -0,0 +1,23 @@
/*
* 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.
*/
/**
* This provides integration of a {@link okhttp3.mockwebserver.MockWebServer} and the
* {@link org.springframework.core.env.Environment}.
*
* @author Rob Winch
*/
package org.springframework.boot.env;
@@ -0,0 +1 @@
org.springframework.boot.env.EnvironmentPostProcessor=org.springframework.boot.env.MockWebServerEnvironmentPostProcessor
@@ -0,0 +1,6 @@
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json
@@ -0,0 +1,134 @@
/*
* 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 example;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt;
/**
* @author Josh Cummings
*/
@WebFluxTest(OAuth2ResourceServerController.class)
@Import(SecurityConfiguration.class)
public class OAuth2ResourceServerControllerTests {
@Autowired
WebTestClient rest;
@MockBean
ReactiveJwtDecoder jwtDecoder;
@Test
void indexGreetsAuthenticatedUser() {
// @formatter:off
this.rest.mutateWith(mockJwt().jwt((jwt) -> jwt.subject("test-subject")))
.get()
.uri("/")
.exchange()
.expectBody(String.class).isEqualTo("Hello, test-subject!");
// @formatter:on
}
@Test
void messageCanBeReadWithScopeMessageReadAuthority() {
// @formatter:off
this.rest.mutateWith(mockJwt().jwt((jwt) -> jwt.claim("scope", "message:read")))
.get()
.uri("/message")
.exchange()
.expectBody(String.class).isEqualTo("secret message");
this.rest.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("SCOPE_message:read")))
.get()
.uri("/message")
.exchange()
.expectBody(String.class).isEqualTo("secret message");
// @formatter:on
}
@Test
void messageCanNotBeReadWithoutScopeMessageReadAuthority() {
// @formatter:off
this.rest.mutateWith(mockJwt())
.get()
.uri("/message")
.exchange()
.expectStatus().isForbidden();
// @formatter:on
}
@Test
void messageCanNotBeCreatedWithoutAnyScope() {
Jwt jwt = jwt().claim("scope", "").build();
when(this.jwtDecoder.decode(anyString())).thenReturn(Mono.just(jwt));
// @formatter:off
this.rest.post()
.uri("/message")
.headers((headers) -> headers.setBearerAuth(jwt.getTokenValue()))
.bodyValue("Hello message")
.exchange()
.expectStatus().isForbidden();
// @formatter:on
}
@Test
void messageCanNotBeCreatedWithScopeMessageReadAuthority() {
Jwt jwt = jwt().claim("scope", "message:read").build();
when(this.jwtDecoder.decode(anyString())).thenReturn(Mono.just(jwt));
// @formatter:off
this.rest.post()
.uri("/message")
.headers((headers) -> headers.setBearerAuth(jwt.getTokenValue()))
.bodyValue("Hello message")
.exchange()
.expectStatus().isForbidden();
// @formatter:on
}
@Test
void messageCanBeCreatedWithScopeMessageWriteAuthority() {
Jwt jwt = jwt().claim("scope", "message:write").build();
when(this.jwtDecoder.decode(anyString())).thenReturn(Mono.just(jwt));
// @formatter:off
this.rest.post()
.uri("/message")
.headers((headers) -> headers.setBearerAuth(jwt.getTokenValue()))
.bodyValue("Hello message")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Message was created. Content: Hello message");
// @formatter:on
}
private Jwt.Builder jwt() {
return Jwt.withTokenValue("token").header("alg", "none");
}
}
@@ -0,0 +1,63 @@
= OAuth 2.0 WebClient (WebFlux) Sample
== GitHub Repositories
This guide provides instructions on setting up the sample application, which leverages WebClient OAuth2 integration to display a list of public GitHub repositories that are accessible to the authenticated user.
This includes repositories owned by the authenticated user, repositories where the authenticated user is a collaborator, and repositories that the authenticated user has access to through an organization membership.
The following sections provide detailed steps for setting up the sample and covers the following topics:
* <<github-register-application,Register OAuth application>>
* <<github-application-config,Configure application.yml>>
* <<github-boot-application,Boot up the application>>
[[github-register-application]]
=== Register OAuth application
To use GitHub's OAuth 2.0 authorization system, you must https://github.com/settings/applications/new[Register a new OAuth application].
When registering the OAuth application, ensure the *Authorization callback URL* is set to `http://localhost:8080/login/oauth2/code/client-id`.
The Authorization callback URL (redirect URI) is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with GitHub and have granted access to the OAuth application on the _Authorize application_ page.
[[github-application-config]]
=== Configure application.yml
Now that you have a new OAuth application with GitHub, you need to configure the sample to use the OAuth application for the _authorization code grant flow_.
To do so:
. Go to `application.yml` and set the following configuration:
+
[source,yaml]
----
spring:
security:
oauth2:
client:
registration: <1>
client-id: <2>
client-id: replace-with-client-id
client-secret: replace-with-client-secret
provider: github
scope: read:user,public_repo
----
+
.OAuth Client properties
====
<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties.
<2> Following the base property prefix is the ID for the `ClientRegistration`, which is github.
====
. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier.
[[github-boot-application]]
=== Boot up the application
Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`.
You are then redirected to the default _auto-generated_ form login page.
Log in using *'user'* (username) and *'password'* (password) or click the link to authenticate with GitHub and then you'll be redirected to GitHub for authentication.
After authenticating with your GitHub credentials, the next page presented to you is "Authorize application".
This page will ask you to *Authorize* the application you created in the previous step.
Click _Authorize application_ to allow the OAuth application to access and display your public repository information.
@@ -0,0 +1,27 @@
plugins {
id 'org.springframework.boot' version '2.2.6.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id "nebula.integtest" version "7.0.9"
id 'java'
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework:spring-webflux'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
implementation 'io.projectreactor.netty:reactor-netty'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'com.squareup.okhttp3:mockwebserver'
}
tasks.withType(Test).configureEach {
useJUnitPlatform()
}
@@ -0,0 +1 @@
spring-security.version=5.4.0.BUILD-SNAPSHOT
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author 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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+104
View File
@@ -0,0 +1,104 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1 @@
@@ -0,0 +1,35 @@
/*
* 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 sample;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* Index controller.
*
* @author Rob Winch
*/
@Controller
public class IndexController {
@GetMapping("/")
String index() {
return "index";
}
}
@@ -0,0 +1,69 @@
/*
* 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 sample;
import reactor.core.publisher.Mono;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.reactive.function.client.WebClient;
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId;
/**
* A controller that demonstrates how to use WebClient with OAuth.
*
* @author Joe Grandja
* @author Rob Winch
*/
@Controller
@RequestMapping(path = { "/webclient", "/public/webclient" })
public class OAuth2WebClientController {
private final WebClient webClient;
public OAuth2WebClientController(WebClient webClient) {
this.webClient = webClient;
}
@GetMapping("/explicit")
String explicit(Model model) {
// @formatter:off
Mono<String> body = this.webClient
.get()
.attributes(clientRegistrationId("client-id"))
.retrieve()
.bodyToMono(String.class);
// @formatter:on
model.addAttribute("body", body);
return "response";
}
@GetMapping("/implicit")
String implicit(Model model) {
// @formatter:off
Mono<String> body = this.webClient
.get()
.retrieve()
.bodyToMono(String.class);
// @formatter:on
model.addAttribute("body", body);
return "response";
}
}
@@ -0,0 +1,33 @@
/*
* 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 sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* OAuth WebClient application.
*
* @author Joe Grandja
*/
@SpringBootApplication
public class OAuth2WebClientWebFluxApplication {
public static void main(String[] args) {
SpringApplication.run(OAuth2WebClientWebFluxApplication.class, args);
}
}
@@ -0,0 +1,71 @@
/*
* 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 sample;
import reactor.core.publisher.Mono;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.reactive.function.client.WebClient;
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;
/**
* Demonstrates usage of {@link RegisteredOAuth2AuthorizedClient}.
*
* @author Joe Grandja
* @author Rob Winch
*/
@Controller
@RequestMapping(path = { "/annotation", "/public/annotation" })
public class RegisteredOAuth2AuthorizedClientController {
private final WebClient webClient;
public RegisteredOAuth2AuthorizedClientController(WebClient webClient) {
this.webClient = webClient;
}
@GetMapping("/explicit")
String explicit(Model model,
@RegisteredOAuth2AuthorizedClient("client-id") OAuth2AuthorizedClient authorizedClient) {
// @formatter:off
Mono<String> body = this.webClient.get()
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class);
// @formatter:on
model.addAttribute("body", body);
return "response";
}
@GetMapping("/implicit")
String implicit(Model model, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
// @formatter:off
Mono<String> body = this.webClient.get()
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class);
// @formatter:on
model.addAttribute("body", body);
return "response";
}
}
@@ -0,0 +1,63 @@
/*
* 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 sample;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.SecurityWebFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* Example of security configuration for oauth client usage.
*
* @author Rob Winch
*/
@EnableWebFluxSecurity
public class SecurityConfiguration {
@Bean
SecurityWebFilterChain configure(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((exchanges) -> exchanges
.pathMatchers("/", "/public/**").permitAll()
.anyExchange().authenticated()
)
.oauth2Login(withDefaults())
.formLogin(withDefaults())
.oauth2Client(withDefaults());
// @formatter:on
return http.build();
}
@Bean
MapReactiveUserDetailsService userDetailsService() {
// @formatter:off
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
// @formatter:on
return new MapReactiveUserDetailsService(userDetails);
}
}
@@ -0,0 +1,77 @@
/*
* 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 sample;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.web.reactive.function.client.WebClient;
/**
* OAuth WebClient configuration.
*
* @author Rob Winch
* @since 5.1
*/
@Configuration
public class WebClientConfiguration {
@Value("${resource-uri}")
String uri;
@Bean
WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager);
oauth.setDefaultOAuth2AuthorizedClient(true);
// @formatter:off
return WebClient.builder()
.baseUrl(this.uri)
.filter(oauth)
.build();
// @formatter:on
}
@Bean
ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
// @formatter:off
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
// @formatter:on
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
@@ -0,0 +1,21 @@
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
# org.springframework.boot.autoconfigure: DEBUG
spring:
thymeleaf:
cache: false
security:
oauth2:
client:
registration:
client-id:
client-id: replace-with-client-id
client-secret: replace-with-client-secret
provider: github
scope: read:user,public_repo
resource-uri: https://api.github.com/user/repos
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<title>OAuth2 WebClient Showcase</title>
<meta charset="utf-8" />
</head>
<body>
<a th:href="@{/logout}">Log Out</a>
<h1>Examples</h1>
<h2>@RegisteredOAuth2AuthorizedClient</h2>
<p>
Examples on RegisteredOAuth2AuthorizedClientController
<h3>Authenticated</h3>
<ul>
<li><a th:href="@{/annotation/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/annotation/implicit}">Implicit</a> - Use the currently logged in user's OAuth Token. This will
only work if the user authenticates with oauth2Login and the token provided is the correct token provided at
log in is authorized.</li>
</ul>
<h3>Public</h3>
<ul>
<li><a th:href="@{/public/annotation/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/public/annotation/implicit}">Implicit</a> - This will fail if the user is not authenticated.
Since it is mapped to permitAll, it is going to fail unless the user already took an action to log in and then
authenticates with oauth2Login()</li>
</ul>
<h2>ServerOAuth2AuthorizedClientExchangeFilterFunction</h2>
<p>
Examples on OAuth2WebClientController that demonstrate how to use ServerOAuth2AuthorizedClientExchangeFilterFunction
<h3>Authenticated</h3>
<ul>
<li><a th:href="@{/webclient/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/webclient/implicit}">Implicit</a> - Use the currently logged in user's OAuth Token. This will
only work if the user authenticates with oauth2Login and the token provided is the correct token provided at
log in is authorized.</li>
</ul>
<h3>Public</h3>
<ul>
<li><a th:href="@{/public/webclient/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/public/webclient/implicit}">Implicit</a> - This will fail if the user is not authenticated.
Since it is mapped to permitAll, it is going to fail unless the user already took an action to log in and then
authenticates with oauth2Login()</li>
</ul>
</body>
</html>
@@ -0,0 +1,31 @@
<!--
~ 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.
-->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>OAuth2 WebClient Showcase</title>
<meta charset="utf-8" />
</head>
<body>
<a th:href="@{/}">Back</a>
<h1>Response</h1>
<pre><code id="json" class="json" th:text="${body}"></code></pre>
<script>
json.innerHTML = JSON.stringify(JSON.parse(json.innerHTML), null, 4);
</script>
</body>
</html>
@@ -0,0 +1,122 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.WebClient;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Client;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Login;
@WebFluxTest
@Import({ SecurityConfiguration.class, OAuth2WebClientController.class })
@AutoConfigureWebTestClient
public class OAuth2WebClientControllerTests {
private static MockWebServer web = new MockWebServer();
@Autowired
private WebTestClient client;
@MockBean
ReactiveClientRegistrationRepository clientRegistrationRepository;
@AfterAll
static void shutdown() throws Exception {
web.shutdown();
}
@Test
void explicitWhenAuthenticatedThenUsesClientIdRegistration() throws Exception {
web.enqueue(new MockResponse().setBody("body").setResponseCode(200));
// @formatter:off
this.client.mutateWith(mockOAuth2Login())
.mutateWith(mockOAuth2Client("client-id"))
.get()
.uri("/webclient/explicit")
.exchange()
.expectStatus().isOk();
// @formatter:on
}
@Test
void implicitWhenAuthenticatedThenUsesDefaultRegistration() throws Exception {
web.enqueue(new MockResponse().setBody("body").setResponseCode(200));
// @formatter:off
this.client.mutateWith(mockOAuth2Login())
.get()
.uri("/webclient/implicit")
.exchange()
.expectStatus().isOk();
// @formatter:on
}
@Test
void publicExplicitWhenAuthenticatedThenUsesClientIdRegistration() throws Exception {
web.enqueue(new MockResponse().setBody("body").setResponseCode(200));
// @formatter:off
this.client.mutateWith(mockOAuth2Client("client-id"))
.get()
.uri("/public/webclient/explicit")
.exchange()
.expectStatus().isOk();
// @formatter:on
}
@Test
void publicImplicitWhenAuthenticatedThenUsesDefaultRegistration() throws Exception {
web.enqueue(new MockResponse().setBody("body").setResponseCode(200));
// @formatter:off
this.client.mutateWith(mockOAuth2Login())
.get()
.uri("/public/webclient/implicit")
.exchange()
.expectStatus().isOk();
// @formatter:on
}
@Configuration
static class WebClientConfig {
@Bean
WebClient web() {
return WebClient.create(web.url("/").toString());
}
@Bean
ServerOAuth2AuthorizedClientRepository authorizedClientRepository() {
return new WebSessionServerOAuth2AuthorizedClientRepository();
}
}
}
@@ -0,0 +1,46 @@
/*
* 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 sample;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* @author Rob Winch
*/
@SpringBootTest
@AutoConfigureWebTestClient
public class OAuth2WebClientWebFluxApplicationTests {
@Autowired
private WebTestClient client;
@Test
void annotationExplicitWhenNotAuthenticatedThenLoginRequested() {
// @formatter:off
this.client.get()
.uri("/annotation/explicit")
.exchange()
.expectStatus().is3xxRedirection();
// @formatter:on
}
}
@@ -0,0 +1,122 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.WebClient;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Client;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Login;
@WebFluxTest
@Import({ SecurityConfiguration.class, RegisteredOAuth2AuthorizedClientController.class })
@AutoConfigureWebTestClient
public class RegisteredOAuth2AuthorizedClientControllerTests {
private static MockWebServer web = new MockWebServer();
@Autowired
private WebTestClient client;
@MockBean
ReactiveClientRegistrationRepository clientRegistrationRepository;
@AfterAll
static void shutdown() throws Exception {
web.shutdown();
}
@Test
void annotationExplicitWhenAuthenticatedThenUsesClientIdRegistration() throws Exception {
web.enqueue(new MockResponse().setBody("body").setResponseCode(200));
// @formatter:off
this.client.mutateWith(mockOAuth2Login())
.mutateWith(mockOAuth2Client("client-id"))
.get()
.uri("/annotation/explicit")
.exchange()
.expectStatus().isOk();
// @formatter:on
}
@Test
void annotationImplicitWhenAuthenticatedThenUsesDefaultRegistration() throws Exception {
web.enqueue(new MockResponse().setBody("body").setResponseCode(200));
// @formatter:off
this.client.mutateWith(mockOAuth2Login())
.get()
.uri("/annotation/implicit")
.exchange()
.expectStatus().isOk();
// @formatter:on
}
@Test
void publicAnnotationExplicitWhenAuthenticatedThenUsesClientIdRegistration() throws Exception {
web.enqueue(new MockResponse().setBody("body").setResponseCode(200));
// @formatter:off
this.client.mutateWith(mockOAuth2Client("client-id"))
.get()
.uri("/public/annotation/explicit")
.exchange()
.expectStatus().isOk();
// @formatter:on
}
@Test
void publicAnnotationImplicitWhenAuthenticatedThenUsesDefaultRegistration() throws Exception {
web.enqueue(new MockResponse().setBody("body").setResponseCode(200));
// @formatter:off
this.client.mutateWith(mockOAuth2Login())
.get()
.uri("/public/annotation/implicit")
.exchange()
.expectStatus().isOk();
// @formatter:on
}
@Configuration
static class WebClientConfig {
@Bean
WebClient web() {
return WebClient.create(web.url("/").toString());
}
@Bean
ServerOAuth2AuthorizedClientRepository authorizedClientRepository() {
return new WebSessionServerOAuth2AuthorizedClientRepository();
}
}
}