Laboratorio de autenticación OAuth 2 y JWT con Spring Security Authorization Server.
Instrucciones
- Crear el proyecto con Spring Initializr https://start.spring.io/.
Nos aseguramos de agregar los siquientes campos:
Project: Maven Project
Language: Java
Spring Boot: 3.2.1
Project Metadata:
Group: com.example
Artifact: auth-server
Name: auth-server
Description: Demo project for Spring Boot
Package name: com.example
Packaging: Jar
Java: 17
Dependencies:
Spring Boot DevTools
Spring Web
Spring Security
Spring Security OAuth2 Authorization Server
Generamos el proyecto y lo descomprimimos para empezar.
Puedes utilizar este link
- Abrimos el proyecto en nuestro IDE (Eclipse, IntelliJ, NetBeans, etc). En este tutorial se utilizará IntelliJ IDEA.
Canviamos la version de Spring Boot por 3.2.1 en el archivo pom.xml
- Creamos un nuevo paquete llamado auth y dentro de este paquete creamos una clase llamada SecurityConfig.
Podemos utilizar la documentación oficial en el siguiente link https://docs.spring.io/spring-authorization-server/reference/getting-started.html
package com.example.auth;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
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.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.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
.applyDefaultSecurity(http);
OAuth2AuthorizationServerConfiguration.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
http.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
// Form login handles the redirect to the login page from the
// authorization server filter chain
.csrf(csrf -> csrf.disable())
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
= User.builder()
UserDetails userDetails .username("juan")
.password("{noop}12345")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
= RegisteredClient.withId(UUID.randomUUID().toString())
RegisteredClient oidcClient .clientId("client-app")
.clientSecret("{noop}12345")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/client-app")
.redirectUri("http://127.0.0.1:8080/authorized")
.postLogoutRedirectUri("http://127.0.0.1:8080/logout")
.scope("read")
.scope("write")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
.build();
return new InMemoryRegisteredClientRepository(oidcClient);
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
= new JWKSet(rsaKey);
JWKSet jwkSet return new ImmutableJWKSet<>(jwkSet);
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
.initialize(2048);
keyPairGenerator= keyPairGenerator.generateKeyPair();
keyPair }
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
- Creamos una configuración para el puerto de autorización en el archivo application.properties.
server.port=9000
- Creamos un segundo proyecto con Spring Initializr https://start.spring.io/. Que servirá como cliente de nuestro servidor de autorización.
Nos aseguramos de agregar los siquientes campos:
Project: Maven Project
Language: Java
Spring Boot: 3.2.1
Project Metadata:
Group: com.example
Artifact: client
Name: client
Description: Demo project for Spring Boot
Package name: com.example
Packaging: Jar
Java: 17
Dependencies:
Spring Boot DevTools
Spring Web
Spring Security
Spring Security OAuth2 Client
Spring Security OAuth2 Resource Server
Podemos utilizar la siguiente url https://start.spring.io/#!type=maven-project&language=java&platformVersion=3.3.0&packaging=jar&jvmVersion=17&groupId=com.example&artifactId=client&name=client&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example&dependencies=devtools,web,security,oauth2-client,oauth2-resource-server
Abrimos el proyecto en una nueva ventana de nuestro IDE.
- Creamos un archivo de configuración en la sección resources llamado application.yml.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: "http://127.0.0.1:9000"
client:
registration:
client-app:
provider: spring
client-id: client-id
client-secret: 12345
authorization-grant-type: autorization_code
redirect-uri: "http://127.0.0.1:8080/authorized"
scope:
- openid
- profile
- read
client-name: client-app
provider:
spring:
issuer-uri: "http://127.0.0.1:9000"
- Creamos un paquete llamado controllers y dentro de este paquete creamos una clase llamada AppController.
package com.example.controller;
import com.example.models.Message;
import org.springframework.web.bind.annotation.*;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@RestController
public class AppController {
@GetMapping("/list")
public List<Message> list(){
return Collections.singletonList(new Message( "Test List"));
}
@PostMapping("/create")
public Message create(@RequestBody Message message){
System.out.println("mensaje guardado: " + message);
return message;
}
@GetMapping("/authorized")
public Map<String, String> authorized(@RequestParam String code){
return Collections.singletonMap("code", code);
}
}
- Creamos un nuevo paquete llamado models y dentro de este paquete creamos una clase llamada Message.
package com.example.models;
public class Message {
private String text;
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Message() {
}
public Message(String text) {
this.text = text;
}
}
- Creamos un nuevo paquete llamado auth y dentro de este paquete creamos una clase llamada SecurityConfig.
package com.example.auth;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain(HttpSecurity http) throws Exception {
SecurityFilterChain
.authorizeHttpRequests((authHttp) -> authHttp
http.requestMatchers(HttpMethod.GET,"/autorized").permitAll()
.requestMatchers(HttpMethod.GET,"/list").hasAnyAuthority("SCOPE_read", "SCOPE_write")
.requestMatchers(HttpMethod.POST, "/create").hasAnyAuthority("SCOPE_write")
.anyRequest().authenticated())
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2Login(login -> login.loginPage("/oauth2/authorization/client-app"))
.oauth2Client(withDefaults())
.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults()));
return http.build();
}
}
Levantamos el servidor de autorización y el cliente.
Ingresamos a la siguiente url http://127.0.0.1:9000/oauth2/authorization/client-app y nos autenticamos con el usuario juan y la contraseña 12345.
- Una vez autenticados, realiza una redirección
- Podemos comprobar con Postman que el servidor de autorización está funcionando correctamente.
- Podemos comprobar con Postman que el cliente está funcionando correctamente.
- Podemos comprobar con Postman que el cliente está funcionando correctamente.
Con esto hemos terminado el laboratorio de autenticación OAuth 2 y JWT con Spring Security Authorization Server.
Hay un error en el código del cliente, por favor resuelve el error y vuelve a ejecutar el laboratorio.
Reto
Implementar un servidor de autorización con Spring Security Authorization Server y un cliente con Spring Security OAuth2 Client y Spring Security OAuth2 Resource Server.
Conclusiones
Spring Security Authorization Server es una herramienta muy útil para la autenticación de aplicaciones.
Spring Security OAuth2 Client y Spring Security OAuth2 Resource Server son herramientas muy útiles para la autenticación de aplicaciones.
Oauth2 es un protocolo de autorización que permite a una aplicación obtener acceso a los recursos de un usuario sin tener que almacenar las credenciales del usuario.