AuthorizationServerConfig.java
package com.albertsilva.dev.asjcatalog.security.oauth2.authorization.config;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
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.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.web.SecurityFilterChain;
import com.albertsilva.dev.asjcatalog.security.oauth2.grant.password.CustomPasswordAuthenticationConverter;
import com.albertsilva.dev.asjcatalog.security.oauth2.grant.password.CustomPasswordAuthenticationProvider;
import com.albertsilva.dev.asjcatalog.security.userdetails.AuthenticatedUser;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
/**
* Configuração do <b>Authorization Server</b> (Spring Authorization Server),
* que roda no mesmo processo do Resource Server e emite os tokens usados pela
* API.
*
* <p>
* <b>O que esta classe configura:</b>
* </p>
* <ul>
* <li>a cadeia de filtros dos endpoints OAuth2 ({@code /oauth2/**} e
* {@code /.well-known/**}), registrando no <em>token endpoint</em> o conversor
* e o provider do grant {@code password} customizado
* ({@link CustomPasswordAuthenticationConverter} e
* {@link CustomPasswordAuthenticationProvider});</li>
* <li>um <b>único cliente registrado</b>, em memória, com os grants
* {@code password} e {@code refresh_token} e os escopos {@code read} e
* {@code write};</li>
* <li>as configurações de token: access token JWT autocontido, com validade
* lida de {@code security.jwt.duration} (segundos), e refresh token de 30 dias
* com rotação ({@code reuseRefreshTokens = false});</li>
* <li>o gerador de tokens (JWT, token de acesso opaco e refresh token) e o
* customizador que acrescenta ao JWT os claims {@code authorities},
* {@code userId} e {@code username};</li>
* <li>a chave RSA/JWK e o {@link JwtDecoder}, usados também pelo Resource
* Server ({@code ResourceServerConfig}).</li>
* </ul>
*
* <p>
* <b>Estado em memória:</b> o cliente, as autorizações (incluindo os refresh
* tokens), os consentimentos e o par de chaves RSA existem apenas na JVM em
* execução e são recriados a cada inicialização. Consequências técnicas: um
* reinício invalida todos os tokens emitidos, e duas instâncias da aplicação
* teriam chaves e autorizações diferentes.
* </p>
*
* <p>
* <b>O que não está configurado:</b> {@code authorization_code},
* {@code client_credentials}, consentimento, URIs de redirecionamento, OpenID
* Connect, persistência das autorizações e qualquer mecanismo próprio de
* revogação ou de logout.
* </p>
*
* <p>
* <b>Propriedades usadas:</b> {@code security.client-id},
* {@code security.client-secret} (valores sensíveis, com padrão de
* desenvolvimento em {@code application.properties}) e
* {@code security.jwt.duration}.
* </p>
*/
@Configuration
public class AuthorizationServerConfig {
/** Identificador do cliente OAuth2 ({@code security.client-id}). */
@Value("${security.client-id}")
private String clientId;
/** Segredo do cliente OAuth2 ({@code security.client-secret}); dado sensível, nunca deve ser registrado em log. */
@Value("${security.client-secret}")
private String clientSecret;
/** Validade do access token, em segundos ({@code security.jwt.duration}). */
@Value("${security.jwt.duration}")
private Integer jwtDurationSeconds;
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
/**
* @param userDetailsService serviço que carrega o usuário no login (na
* aplicação, {@code UserService}); repassado ao
* provider do grant {@code password}
* @param passwordEncoder codificador de senhas: usado para codificar o
* segredo do cliente e repassado ao provider, que o
* usa para conferir a senha do usuário
*/
public AuthorizationServerConfig(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
/**
* Cadeia de filtros dos endpoints do Authorization Server ({@code @Order(2)}).
*
* <p>
* <b>Escopo:</b> só atende requisições que casam com {@code /oauth2/**} ou
* {@code /.well-known/**}; as demais seguem para a cadeia do Resource Server
* ({@code @Order(3)}).
* </p>
*
* <p>
* <b>Configuração:</b> aplica {@code OAuth2AuthorizationServerConfigurer} com os
* padrões e registra, no token endpoint, um
* {@link CustomPasswordAuthenticationConverter} e um
* {@link CustomPasswordAuthenticationProvider} (criados com {@code new}, não
* são beans), este último com {@link #authorizationService()},
* {@link #tokenGenerator()}, o {@code UserDetailsService} e o
* {@code PasswordEncoder}. Como são acrescentados ao token endpoint, os
* conversores e providers padrão do framework continuam ativos: é assim que o
* grant {@code refresh_token} é tratado, sem código próprio da aplicação.
* Também habilita {@code oauth2ResourceServer().jwt()} nesta cadeia.
* </p>
*
* <p>
* Não há {@code authorizeHttpRequests} nesta cadeia: a autenticação do cliente
* no token endpoint é feita pelos filtros do próprio Authorization Server.
* </p>
*
* @param http construtor de segurança HTTP
* @return cadeia de filtros do Authorization Server
* @throws Exception se a configuração falhar
*/
@Bean
@Order(2)
public SecurityFilterChain asSecurityFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/oauth2/**", "/.well-known/**")
.with(OAuth2AuthorizationServerConfigurer.authorizationServer(), Customizer.withDefaults());
// @formatter:off
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter
(new CustomPasswordAuthenticationConverter()).authenticationProvider
(new CustomPasswordAuthenticationProvider(authorizationService(), tokenGenerator(), userDetailsService, passwordEncoder)));
http.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt(Customizer.withDefaults()));
// @formatter:on
return http.build();
}
/**
* Armazenamento <b>em memória</b> das autorizações emitidas (cada login gera
* uma, com o access token, o refresh token e o principal). Não é persistido:
* os dados se perdem ao reiniciar e não são compartilhados entre instâncias.
*
* @return serviço de autorizações em memória
*/
@Bean
public OAuth2AuthorizationService authorizationService() {
return new InMemoryOAuth2AuthorizationService();
}
/**
* Armazenamento em memória dos consentimentos. Mantido pelo framework, mas o
* cliente registrado não exige consentimento e nenhum fluxo do projeto o usa.
*
* @return serviço de consentimentos em memória
*/
@Bean
public OAuth2AuthorizationConsentService oAuth2AuthorizationConsentService() {
return new InMemoryOAuth2AuthorizationConsentService();
}
/**
* Registra, em memória, o <b>único</b> cliente OAuth2 da aplicação.
*
* <ul>
* <li>identificador interno: UUID novo a cada inicialização;</li>
* <li>{@code clientId} e segredo vêm das propriedades; o segredo é codificado
* com o {@code PasswordEncoder} (BCrypt) na inicialização;</li>
* <li>escopos: {@code read} e {@code write};</li>
* <li>grants: {@code password} (grant customizado, criado como
* {@code new AuthorizationGrantType("password")}) e {@code refresh_token};</li>
* <li>usa os beans {@link #tokenSettings()} e {@link #clientSettings()};</li>
* <li>não declara método de autenticação do cliente (vale o padrão do
* framework; os testes de integração usam HTTP Basic), nem URIs de
* redirecionamento.</li>
* </ul>
*
* @return repositório em memória com esse único cliente
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
// @formatter:off
RegisteredClient registeredClient = RegisteredClient
.withId(UUID.randomUUID().toString())
.clientId(clientId)
.clientSecret(passwordEncoder.encode(clientSecret))
.scope("read")
.scope("write")
.authorizationGrantType(
new AuthorizationGrantType("password")
)
.authorizationGrantType(
AuthorizationGrantType.REFRESH_TOKEN
)
.tokenSettings(tokenSettings())
.clientSettings(clientSettings())
.build();
// @formatter:on
return new InMemoryRegisteredClientRepository(registeredClient);
}
/**
* Configurações dos tokens do cliente registrado.
*
* <ul>
* <li>access token no formato {@code SELF_CONTAINED} (JWT);</li>
* <li>validade do access token: {@code security.jwt.duration} segundos (padrão
* de 86400 em {@code application.properties});</li>
* <li>validade do refresh token: <b>30 dias, fixa no código</b>;</li>
* <li>{@code reuseRefreshTokens = false}: a cada renovação, um novo refresh
* token é emitido e o anterior deixa de ser aceito.</li>
* </ul>
*
* @return configurações de token
*/
@Bean
public TokenSettings tokenSettings() {
// @formatter:off
return TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.accessTokenTimeToLive(Duration.ofSeconds(jwtDurationSeconds))
.refreshTokenTimeToLive(Duration.ofDays(30))
.reuseRefreshTokens(false)
.build();
// @formatter:on
}
/**
* Configurações do cliente com os valores padrão do framework (sem exigir
* consentimento, sem PKCE obrigatório etc.).
*
* @return configurações padrão de cliente
*/
@Bean
public ClientSettings clientSettings() {
return ClientSettings.builder().build();
}
/**
* Configurações do servidor com os valores padrão: caminhos padrão dos
* endpoints ({@code /oauth2/token}, {@code /oauth2/jwks} etc.) e emissor
* ({@code iss}) não fixado, resolvido a partir da requisição.
*
* @return configurações padrão do servidor
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
/**
* Monta o gerador de tokens usado pelo provider do grant {@code password} e
* pelo fluxo de refresh.
*
* <p>
* É um {@code DelegatingOAuth2TokenGenerator} com: {@code JwtGenerator}
* (assina o JWT com o {@code NimbusJwtEncoder} e a chave de {@link #jwkSource()},
* aplicando {@link #tokenCustomizer()}); {@code OAuth2AccessTokenGenerator}
* (token opaco, não usado porque o formato é autocontido) e
* {@code OAuth2RefreshTokenGenerator} (refresh token opaco).
* </p>
*
* @return gerador de tokens
*/
@Bean
public OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator() {
NimbusJwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource());
JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
jwtGenerator.setJwtCustomizer(tokenCustomizer());
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}
/**
* Acrescenta claims customizados ao <b>access token</b> JWT.
*
* <p>
* <b>Origem dos dados:</b> o {@code principal} do contexto de geração é cast
* para {@code OAuth2ClientAuthenticationToken}, e seus {@code details} para
* {@link AuthenticatedUser} (colocado ali pelo provider do grant
* {@code password}). Nada é relido do banco neste ponto.
* </p>
*
* <p>
* <b>Claims acrescentados</b> (somente quando o tipo de token é
* {@code access_token}):
* </p>
* <ul>
* <li>{@code authorities}: lista de textos, uma por authority do usuário
* (por exemplo, {@code ROLE_ADMIN});</li>
* <li>{@code userId}: identificador numérico do usuário;</li>
* <li>{@code username}: valor de {@code username} informado no login.</li>
* </ul>
*
* <p>
* No fluxo de refresh, os mesmos dados são reaproveitados do principal guardado
* na autorização em memória (os testes de integração confirmam que os claims
* são mantidos), portanto refletem o estado do usuário <b>no momento do
* login</b>, e não o estado atual.
* </p>
*
* @return customizador de claims do JWT
*/
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
return context -> {
OAuth2ClientAuthenticationToken principal = context.getPrincipal();
AuthenticatedUser user = (AuthenticatedUser) principal.getDetails();
List<String> authorities = user.getAuthorities().stream().map(x -> x.getAuthority()).toList();
if (context.getTokenType().getValue().equals("access_token")) {
// @formatter:off
context.getClaims()
.claim("authorities", authorities)
.claim("userId", user.getId())
.claim("username", user.getUsername());
// @formatter:on
}
};
}
/**
* Decodificador de JWT baseado na mesma {@code JWKSource} que assina os tokens
* ({@code OAuth2AuthorizationServerConfiguration.jwtDecoder}).
*
* <p>
* É o <b>único</b> bean {@link JwtDecoder}, portanto é o que as cadeias do
* Authorization Server e do Resource Server usam ({@code jwt(withDefaults())}).
* Valida a assinatura com as chaves em memória e, pelos validadores padrão, a
* expiração; não há configuração de validação de emissor nem de audiência, e
* o estado do usuário (ativo, senha) não é consultado.
* </p>
*
* @param jwkSource fonte das chaves de assinatura
* @return decodificador de JWT
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* Fonte de chaves JWK com <b>uma única chave RSA</b> gerada na inicialização
* (ver {@code generateRsa}).
*
* <p>
* A chave não é lida de arquivo nem de propriedade e não é persistida: a cada
* inicialização há uma nova chave (e um novo {@code kid}); tokens assinados
* antes de um reinício deixam de validar, e instâncias diferentes assinariam
* com chaves diferentes. A chave pública fica disponível no endpoint JWKS do
* Authorization Server (caminho padrão {@code /oauth2/jwks}).
* </p>
*
* @return fonte que seleciona a chave do conjunto em memória
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
/**
* Gera um par RSA (2048 bits) e o empacota como {@link RSAKey}, com a chave
* privada incluída e um {@code kid} aleatório (UUID).
*
* @return chave RSA nova a cada chamada
*/
private static RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();
}
/**
* Gera o par de chaves com {@link KeyPairGenerator} ({@code "RSA"}, 2048
* bits).
*
* @return par de chaves RSA
* @throws IllegalStateException se a geração falhar
*/
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;
}
}