Laboratorio de autenticación OAuth 2 y JWT con Spring Security Authorization Server.

Instrucciones

  1. 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.

Tip

Puedes utilizar este link

  1. Abrimos el proyecto en nuestro IDE (Eclipse, IntelliJ, NetBeans, etc). En este tutorial se utilizará IntelliJ IDEA.
Tip

Canviamos la version de Spring Boot por 3.2.1 en el archivo pom.xml

  1. 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 {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .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() {
        UserDetails userDetails = User.builder()
                .username("juan")
                .password("{noop}12345")
                .roles("USER")
                .build();

        return new InMemoryUserDetailsManager(userDetails);
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .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();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    private static 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;
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }

}
  1. Creamos una configuración para el puerto de autorización en el archivo application.properties.
server.port=9000
  1. 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.

  1. 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"
  1. 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);
    }
}
  1. 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;
    }
}
  1. 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 SecurityFilterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests((authHttp) -> authHttp
                .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();
    }
}
  1. Levantamos el servidor de autorización y el cliente.

  2. 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.

  1. Una vez autenticados, realiza una redirección
  1. Podemos comprobar con Postman que el servidor de autorización está funcionando correctamente.
  1. Podemos comprobar con Postman que el cliente está funcionando correctamente.
  1. 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.

Warning

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.

Referencias