10 Commits

Author SHA1 Message Date
Joe Grandja
ded978132d Remove Jenkinsfile 2021-09-27 10:11:45 -04:00
Joe Grandja
8a66816d42 Remove issuer-uri configuration 2021-03-12 09:44:34 -05:00
Daniel Garnier-Moiroux
dd4bdc4b1b Add implementation notes and questions/considerations 2021-03-12 06:14:48 -05:00
Daniel Garnier-Moiroux
948a5e1e18 Dynamic port on authz + resource server 2021-03-12 06:14:48 -05:00
Daniel Garnier-Moiroux
75520d778b BeforeClass/AfterClass init of auth/resource server 2021-03-12 06:14:48 -05:00
Daniel Garnier-Moiroux
01c3ebdecb Integration test launching both servers 2021-03-12 06:14:48 -05:00
Daniel Garnier-Moiroux
8fedbb0d97 Back to master 2021-03-12 06:14:48 -05:00
Daniel Garnier-Moiroux
5566fd332c SpringApplicationBuilder instead of threads 2021-03-12 06:14:48 -05:00
Daniel Garnier-Moiroux
d527cd25b1 Resource server running in a thread of the Auth server 2021-03-12 06:14:48 -05:00
Daniel Garnier-Moiroux
2445eeeea8 Authorization server & resource server combined 2021-03-12 06:14:48 -05:00
15 changed files with 155 additions and 116 deletions

104
Jenkinsfile vendored
View File

@@ -1,104 +0,0 @@
def projectProperties = [
[$class: 'BuildDiscarderProperty',
strategy: [$class: 'LogRotator', numToKeepStr: '5']],
pipelineTriggers([cron('@daily')])
]
properties(projectProperties)
def SUCCESS = hudson.model.Result.SUCCESS.toString()
currentBuild.result = SUCCESS
def GRADLE_ENTERPRISE_CACHE_USER = usernamePassword(credentialsId: 'gradle_enterprise_cache_user',
passwordVariable: 'GRADLE_ENTERPRISE_CACHE_PASSWORD',
usernameVariable: 'GRADLE_ENTERPRISE_CACHE_USERNAME')
def GRADLE_ENTERPRISE_SECRET_ACCESS_KEY = string(credentialsId: 'gradle_enterprise_secret_access_key',
variable: 'GRADLE_ENTERPRISE_ACCESS_KEY')
def SPRING_SIGNING_SECRING = file(credentialsId: 'spring-signing-secring.gpg', variable: 'SIGNING_KEYRING_FILE')
def SPRING_GPG_PASSPHRASE = string(credentialsId: 'spring-gpg-passphrase', variable: 'SIGNING_PASSWORD')
def OSSRH_CREDENTIALS = usernamePassword(credentialsId: 'oss-token', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USERNAME')
def ARTIFACTORY_CREDENTIALS = usernamePassword(credentialsId: '02bd1690-b54f-4c9f-819d-a77cb7a9822c', usernameVariable: 'ARTIFACTORY_USERNAME', passwordVariable: 'ARTIFACTORY_PASSWORD')
def JENKINS_PRIVATE_SSH_KEY = file(credentialsId: 'docs.spring.io-jenkins_private_ssh_key', variable: 'DEPLOY_SSH_KEY')
def SONAR_LOGIN_CREDENTIALS = string(credentialsId: 'spring-sonar.login', variable: 'SONAR_LOGIN')
def jdkEnv(String jdk = 'jdk8') {
def jdkTool = tool(jdk)
return "JAVA_HOME=${ jdkTool }"
}
try {
parallel check: {
stage('Check') {
node {
checkout scm
sh "git clean -dfx"
try {
withCredentials([ARTIFACTORY_CREDENTIALS,
GRADLE_ENTERPRISE_CACHE_USER,
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY]) {
withEnv([jdkEnv(),
"GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USERNAME}",
"GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PASSWORD}",
"GRADLE_ENTERPRISE_ACCESS_KEY=${GRADLE_ENTERPRISE_ACCESS_KEY}"]) {
sh "./gradlew check -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --stacktrace"
}
}
} catch(Exception e) {
currentBuild.result = 'FAILED: check'
throw e
} finally {
junit '**/build/test-results/*/*.xml'
}
}
}
}
if(currentBuild.result == 'SUCCESS') {
parallel artifacts: {
stage('Deploy Artifacts') {
node {
checkout scm
sh "git clean -dfx"
withCredentials([SPRING_SIGNING_SECRING,
SPRING_GPG_PASSPHRASE,
OSSRH_CREDENTIALS,
ARTIFACTORY_CREDENTIALS,
GRADLE_ENTERPRISE_CACHE_USER,
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY]) {
withEnv([jdkEnv(),
"GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USERNAME}",
"GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PASSWORD}",
"GRADLE_ENTERPRISE_ACCESS_KEY=${GRADLE_ENTERPRISE_ACCESS_KEY}"]) {
sh "./gradlew deployArtifacts finalizeDeployArtifacts -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password='$SIGNING_PASSWORD' -PossrhUsername=$OSSRH_USERNAME -PossrhPassword=$OSSRH_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --stacktrace"
}
}
}
}
}
}
} catch(Exception e) {
currentBuild.result = 'FAILED: deploys'
throw e
} finally {
def buildStatus = currentBuild.result
def buildNotSuccess = !SUCCESS.equals(buildStatus)
def lastBuildNotSuccess = !SUCCESS.equals(currentBuild.previousBuild?.result)
if(buildNotSuccess || lastBuildNotSuccess) {
stage('Notifiy') {
node {
final def RECIPIENTS = [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']]
def subject = "${buildStatus}: Build ${env.JOB_NAME} ${env.BUILD_NUMBER} status is now ${buildStatus}"
def details = """The build status changed to ${buildStatus}. For details see ${env.BUILD_URL}"""
emailext (
subject: subject,
body: details,
recipientProviders: RECIPIENTS,
to: "$SPRING_SECURITY_TEAM_EMAILS"
)
}
}
}
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
package sample.authorizationserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.config;
package sample.authorizationserver.config;
import java.util.UUID;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.config;
package sample.authorizationserver.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

View File

@@ -11,4 +11,7 @@ dependencies {
compile 'org.webjars:webjars-locator-core'
compile 'org.webjars:bootstrap:3.4.1'
compile 'org.webjars:jquery:3.4.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation project(':spring-security-samples-boot-oauth2-integrated-authorizationserver')
testImplementation project(':spring-security-samples-boot-oauth2-integrated-resourceserver')
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
package sample.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.config;
package sample.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.config;
package sample.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
package sample.client.web;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
package sample.client.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

View File

@@ -41,7 +41,8 @@ spring:
client-name: messaging-client-client-credentials
provider:
spring:
issuer-uri: http://auth-server:9000
authorization-uri: http://auth-server:9000/oauth2/authorize
token-uri: http://auth-server:9000/oauth2/token
messages:
base-uri: http://localhost:8090/messages

View File

@@ -0,0 +1,139 @@
package sample.client;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.test.web.servlet.MockMvc;
import sample.authorizationserver.OAuth2AuthorizationServerApplication;
import sample.resourceserver.OAuth2ResourceServerApplication;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
// Notes:
//
// Use-case:
// - A user that develops a bunch of services
// - One of the apps is a “frontend” app that acts as the OAuth 2 client
// - The other apps are OAuth 2 Resource Servers providing (I'm just doing one service for the sake of the example)
// - They rely on a single OAuth 2 provider, a OAuth 2.0-compliant authz server (or at least Spring-supported)
// - They want to make an integration/e2e test of the components they develop (client + resource servers)
// - Probably using SpringBootTest
//
// Some early conclusions:
// - Users would need a _nice and easy_ way of starting an authz server, preferably on a dynamic port
// - At the very least on a configurable port
// - Thinking maybe an annotation, e.g. @EnableTestSpringAuthorizationServer
// - They'd need some sort of autoconfiguration of their client application so that it points at the correct (test) auth provider
// - <explore> is there a way that @SpringBootTest, coupled with @EnableTestSpringAuthorizationServer, auto-configures the client app ?
// - <parking lot> what do we do when users have multiple auth providers ?
// - I assume they'd be starting their authorization server through SpringApplicationBuilder but that's very rough
// - They'd also need to point their resource servers at the correct validation mechanism for JWT (jwks uri, OIDC provider configuration uri, OAuth 2.0 server metadata)
// - For now we inject properties through SpringApplicationBuilder
// - They could turn off jwt validation in their resource servers, but I don't think it makes sense for integration testing
// - <explore> Maybe you could autowire some sort of factory for configuring you resource server, linked to the TestSpringAuthorizationServer ?
// - <parking lot> think about public-key jwt validation
//
// Misc notes on the changes on this branch:
// - This was very much quick-and-dirty, just to make it work
// - e.g., I used 3 spring boot applications, but the authz-server could probably be trimmed down to two config files
// - there's no specific isolation of any kind between the apps; they run in the same JVM, in different threads, but with the same classloader
// - I moved packages around to avoid @SpringBootApplication package scanning conflicts
@RunWith(SpringRunner.class)
@SpringBootTest(classes = OAuth2ClientApplication.class)
@ContextConfiguration(initializers= OAuth2ClientApplicationTest.OAuth2ServersProperties.class)
@AutoConfigureMockMvc
public class OAuth2ClientApplicationTest {
private static ConfigurableApplicationContext authorizationServer;
private static ConfigurableApplicationContext resourceServer;
@Autowired
private MockMvc mockMvc;
@BeforeClass
public static void beforeClass() {
// embedded-server-test: RUN
// Run on dynamic port and get that port for the user to use
authorizationServer = new SpringApplicationBuilder(OAuth2AuthorizationServerApplication.class)
.properties("spring.config.name:embeddedauthorizationserver") // Required to avoid clashes in MBean registration
.properties("server.port:0")
.application()
.run();
String authzServerPort = authorizationServer.getEnvironment().getProperty("local.server.port");
// END embedded-server-test: RUN
// Here the user would configure _their_ embedded resource server themselves
// PROVIDED THAT they use the JWK Set URI to validate jwts
// TODO: think about the public key case
// TODO: could we easily provider a TestJwtDecoder that skips validation? SHOULD we ?
resourceServer = new SpringApplicationBuilder(OAuth2ResourceServerApplication.class)
.properties("spring.config.name:embeddedresourceserver")
.properties("spring.security.oauth2.resourceserver.jwt.jwk-set-uri:http://localhost:" + authzServerPort + "/oauth2/jwks")
.properties("server.port:0")
.application()
.run();
String resourceServerPort = resourceServer.getEnvironment().getProperty("local.server.port");
// embedded-server-test: CONFIGURE CLIENT
// Nasty: override some property configurations for the client through static variables
// TODO: find a better way to interact with OAuth2ClientProperties ; maybe auto-configure it ?
OAuth2ServersProperties.AUTH_SERVER_URL = "http://localhost:" + authzServerPort;
// END embedded-server-test: CONFIGURE CLIENT
// Here would be the user configuring their app to point to _their_ resource server
// That's not great, but this allows for dynamic port on the resource server without touching at the client app code
OAuth2ServersProperties.RESOURCE_SERVER_URL = "http://localhost:" + resourceServerPort;
}
@AfterClass
public static void afterClass() {
authorizationServer.stop();
resourceServer.stop();
}
@Test
public void index() throws Exception {
mockMvc
.perform(get("/index"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("Client Credentials")));
}
@Test
public void authorize() throws Exception {
mockMvc
.perform(get("/authorize?grant_type=client_credentials"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("Message 1")));
}
static class OAuth2ServersProperties implements ApplicationContextInitializer<ConfigurableApplicationContext> {
// Helper class to point the clients at the right authz/resource servers
static String AUTH_SERVER_URL;
static String RESOURCE_SERVER_URL;
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(applicationContext,
"spring.security.oauth2.client.provider.spring.authorization-uri=" + AUTH_SERVER_URL + "/oauth2/authorize",
"spring.security.oauth2.client.provider.spring.token-uri=" + AUTH_SERVER_URL + "/oauth2/token",
"messages.base-uri=" + RESOURCE_SERVER_URL + "/messages"
);
}
}
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
package sample.resourceserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.config;
package sample.resourceserver.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
package sample.resourceserver.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;