Add security-oauth2-authorization-server sample
Closes gh-100
This commit is contained in:
@@ -54,6 +54,7 @@ smoke_tests:
|
||||
- scheduled
|
||||
- security-ldap
|
||||
- security-method
|
||||
- security-oauth2-authorization-server
|
||||
- security-oauth2-resource-server
|
||||
- security-thymeleaf
|
||||
- security-webflux
|
||||
|
||||
26
security-oauth2-authorization-server/build.gradle
Normal file
26
security-oauth2-authorization-server/build.gradle
Normal 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()
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user