Add security-oauth2-authorization-server sample

Closes gh-100
This commit is contained in:
Marcus Da Coregio
2022-08-25 11:55:11 -03:00
parent 8c00e6361e
commit 9c3fa307e9
7 changed files with 387 additions and 0 deletions

View File

@@ -54,6 +54,7 @@ smoke_tests:
- scheduled
- security-ldap
- security-method
- security-oauth2-authorization-server
- security-oauth2-resource-server
- security-thymeleaf
- security-webflux

View File

@@ -0,0 +1,26 @@
plugins {
id 'java'
id 'org.springframework.boot'
id 'org.springframework.aot.smoke-test'
id 'org.graalvm.buildtools.native'
}
dependencies {
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.security:spring-security-oauth2-authorization-server:1.0.0-M1")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
aotTestImplementation("org.springframework.boot:spring-boot-starter-json")
aotTestImplementation(project(":aot-smoke-test-support"))
}
aotSmokeTest {
webApplication = true
}
test {
useJUnitPlatform()
}

View File

@@ -0,0 +1,143 @@
package com.example.security.oauth2authorizationserver;
import java.io.IOException;
import java.util.Map;
import java.util.function.Consumer;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.aot.smoketest.support.junit.AotSmokeTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
@AotSmokeTest
class OAuth2AuthorizationServerApplicationAotTests {
private static final String CLIENT_ID = "messaging-client";
private static final String CLIENT_SECRET = "secret";
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void performTokenRequestWhenValidClientCredentialsThenOk(WebTestClient client) {
// @formatter:off
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("grant_type", "client_credentials");
formData.add("scope", "message:read");
client.post().uri("/oauth2/token").headers((headers) -> headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET))
.body(BodyInserters.fromFormData(formData))
.exchange().expectStatus().isOk()
.expectBody().jsonPath("$.access_token").exists()
.jsonPath("$.expires_in").isNumber()
.jsonPath("$.scope").isEqualTo("message:read")
.jsonPath("$.token_type").isEqualTo("Bearer");
// @formatter:on
}
@Test
void performTokenRequestWhenMissingScopeThenOk(WebTestClient client) {
// @formatter:off
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("grant_type", "client_credentials");
formData.add("scope", "message:read message:write");
client.post().uri("/oauth2/token").headers((headers) -> headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET))
.body(BodyInserters.fromFormData(formData))
.exchange().expectStatus().isOk()
.expectBody().jsonPath("$.access_token").exists()
.jsonPath("$.expires_in").isNumber()
.jsonPath("$.scope").isEqualTo("message:read message:write")
.jsonPath("$.token_type").isEqualTo("Bearer");
// @formatter:on
}
@Test
void performTokenRequestWhenInvalidClientCredentialsThenUnauthorized(WebTestClient client) {
// @formatter:off
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("grant_type", "client_credentials");
formData.add("scope", "message:read");
client.post().uri("/oauth2/token").headers(badCredentials())
.body(BodyInserters.fromFormData(formData))
.exchange().expectStatus().isUnauthorized()
.expectBody().jsonPath("$.error").isEqualTo("invalid_client");
// @formatter:on
}
@Test
void performTokenRequestWhenMissingGrantTypeThenUnauthorized(WebTestClient client) {
// @formatter:off
client.post().uri("/oauth2/token").headers(badCredentials())
.exchange().expectStatus().isUnauthorized()
.expectBody().jsonPath("$.error").isEqualTo("invalid_client");
// @formatter:on
}
@Test
void performTokenRequestWhenGrantTypeNotRegisteredThenBadRequest(WebTestClient client) {
// @formatter:off
client.post().uri("/oauth2/token").headers((headers) -> headers.setBasicAuth("login-client", "openid-connect"))
.body(BodyInserters.fromFormData("grant_type", "client_credentials"))
.exchange().expectStatus().isBadRequest()
.expectBody().jsonPath("$.error").isEqualTo("unauthorized_client");
// @formatter:on
}
@Test
void performIntrospectionRequestWhenValidTokenThenOk(WebTestClient client) throws Exception {
// @formatter:off
client.post().uri("/oauth2/introspect").headers((headers) -> headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET))
.body(BodyInserters.fromFormData("token", getAccessToken(client)))
.exchange().expectStatus().isOk()
.expectBody().jsonPath("$.active").isEqualTo("true")
.jsonPath("$.aud[0]").isEqualTo(CLIENT_ID)
.jsonPath("$.client_id").isEqualTo(CLIENT_ID)
.jsonPath("$.exp").isNumber()
.jsonPath("$.iat").isNumber()
.jsonPath("$.iss").isEqualTo("http://localhost:9000")
.jsonPath("$.nbf").isNumber()
.jsonPath("$.scope").isEqualTo("message:read")
.jsonPath("$.sub").isEqualTo(CLIENT_ID)
.jsonPath("$.token_type").isEqualTo("Bearer");
// @formatter:on
}
@Test
void performIntrospectionRequestWhenInvalidCredentialsThenUnauthorized(WebTestClient client) throws Exception {
// @formatter:off
client.post().uri("/oauth2/introspect").headers(badCredentials())
.body(BodyInserters.fromFormData("token", getAccessToken(client)))
.exchange().expectStatus().isUnauthorized()
.expectBody().jsonPath("$.error").isEqualTo("invalid_client");
// @formatter:on
}
private static Consumer<HttpHeaders> badCredentials() {
return (headers) -> headers.setBasicAuth("bad", "password");
}
private String getAccessToken(WebTestClient client) throws IOException {
// @formatter:off
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("grant_type", "client_credentials");
formData.add("scope", "message:read");
byte[] responseBody = client.post().uri("/oauth2/token")
.headers((headers) -> headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET))
.body(BodyInserters.fromFormData(formData))
.exchange().expectStatus().isOk()
.expectBody().jsonPath("$.access_token").exists()
.returnResult().getResponseBody();
// @formatter:on
Map<String, Object> tokenResponse = this.objectMapper.readValue(responseBody, new TypeReference<>() {
});
return tokenResponse.get("access_token").toString();
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.security.oauth2authorizationserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* OAuth Authorization Server Application.
*
* @author Steve Riesenberg
*/
@SpringBootApplication
public class OAuth2AuthorizationServerApplication {
public static void main(String[] args) throws InterruptedException {
SpringApplication.run(OAuth2AuthorizationServerApplication.class, args);
Thread.currentThread().join(); // To be able to measure memory consumption
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.security.oauth2authorizationserver;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
/**
* OAuth Authorization Server Configuration.
*
* @author Steve Riesenberg
*/
@Configuration
@EnableWebSecurity
public class OAuth2AuthorizationServerSecurityConfiguration {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}
@Bean
@Order(2)
public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
// @formatter:on
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
// @formatter:off
RegisteredClient loginClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("login-client")
.clientSecret("{noop}openid-connect")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/login-client")
.redirectUri("http://127.0.0.1:8080/authorized")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("message:read")
.scope("message:write")
.build();
// @formatter:on
return new InMemoryRegisteredClientRepository(loginClient, registeredClient);
}
@Bean
public JWKSource<SecurityContext> jwkSource(KeyPair keyPair) {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// @formatter:off
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
@Bean
public JwtDecoder jwtDecoder(KeyPair keyPair) {
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
}
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().issuer("http://localhost:9000").build();
}
@Bean
public UserDetailsService userDetailsService() {
// @formatter:off
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
// @formatter:on
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}

View File

@@ -0,0 +1,15 @@
package com.example.security.oauth2authorizationserver;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class OAuth2AuthorizationServerApplicationTests {
@Test
void contextLoads() {
}
}

View File

@@ -85,6 +85,7 @@ include "security-method"
include "security-thymeleaf"
include "security-webflux"
include "security-webmvc"
include "security-oauth2-authorization-server"
include "security-oauth2-resource-server"
include "servlet-tomcat"
include "session-jdbc"