Este post faz parte de uma série sobre a construção de um sistema de referência para autenticação e autorização com OAuth2 e OpenID Connect (OIDC) usando Spring Boot e Next.js. O objetivo da série é cobrir as principais funcionalidades de uma aplicação web típica: recursos públicos, recursos protegidos por login, controle de acesso por perfil, e login social via Google (SSO). Neste post vamos abordar o ponto de partida: a construção de um Authorization Server mínimo, com recursos in-memory.
Visão geral - o que você vai aprender
Neste primeiro post, vamos construir o Authorization Server (AS) — o componente central do ecossistema OAuth2, responsável por autenticar usuários e emitir os tokens que controlam o acesso ao sistema.
Vamos utilizar, por enquanto, recursos in-memory para simplificar a implementação, tais como usuários, aplicações clientes e chave RSA. Em estudos futuros, vamos adicionar os devidos repositórios para lidar com esses recursos.
Ao final deste post, teremos um AS mínimo e funcional com:
- Servidor rodando na porta 9000
- Tela de login padrão do Spring Security
- Dois usuários in-memory (ADMIN e OPERATOR)
- Um cliente OAuth2 registrado (o futuro frontend Next.js)
- Fluxo Authorization Code com PKCE
- Tela de consentimento de scopes
- Suporte a OpenID Connect (OIDC)
- Discovery document (
/.well-known/openid-configuration) - Emissão de JWTs assinados com RSA
Contexto: o papel do Authorization Server no ecossistema
Antes de começar a codar, vale entender o lugar do AS na arquitetura. O sistema completo será composto por quatro partes:
┌─────────────┐ Authorization Code + PKCE ┌─────────────────────┐ │ Next.js │ ─────────────────────────────────► │ Authorization │ │ (Cliente) │ ◄───────────────────────────────── │ Server │ └─────────────┘ access_token + id_token └─────────────────────┘ │ ▲ │ Bearer access_token │ federa com ▼ ▼ ┌─────────────┐ ┌─────────────────────┐ │ Resource │ │ Google (OIDC) │ │ Server │ │ │ └─────────────┘ └─────────────────────┘
O AS é o único componente que conhece os usuários e suas senhas. O Resource Server nunca vê senhas — ele só valida o JWT emitido pelo AS. O Next.js nunca fala diretamente com o Google — ele sempre passa pelo AS.
Estrutura da implementação do Authorization Server com Spring Security
A estrutura de implementação possui as seguintes partes:
- Filtros do Spring Security
- SecurityFilterChain - para endpoints do Authorization Server
- SecurityFilterChain - para demais rotas
- Infraestrutura de usuários
- UserDetailsService - para acessar usuários
- PasswordEncoder - para encodar as senhas
- Registro de aplicações clientes
- RegisteredClientRepository - para acessar apps clientes
- Infraestrutura JWT
- JWKSource - para acessar o par de chaves RSA
- JwtDecoder - para validar o acceess token
- Configuração do servidor
- AuthorizationServerSettings
Começando o projeto
Criando o projeto no Spring Initializr
Acesse start.spring.io e configure:
| Campo | Valor |
|---|---|
| Project | Maven |
| Language | Java |
| Spring Boot | 4.x.x |
| Group | com.devsuperior |
| Artifact | authserver |
| Packaging | Jar |
| Java | 25 |
Adicione as seguintes dependências:
- Spring Web (
spring-boot-starter-webmvc) - Spring Security (
spring-boot-starter-security) - OAuth2 Authorization Server (
spring-boot-starter-security-oauth2-authorization-server) - Spring Data JPA (
spring-boot-starter-data-jpa) - H2 Database (
h2)
Clique em Generate, descompacte o arquivo e abra o projeto na IDE.
A estrutura inicial do pom.xml gerado ficará assim (dependências de test omitidas):
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security-oauth2-authorization-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-h2console</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies>
Configurando os profiles e propriedades
O AS precisa rodar na porta 9000 para não colidir com a porta padrão 8080 (que será usada pelo Resource Server).
Vamos usar o padrão de profiles do Spring Boot para separar configurações de ambiente. Edite o application.properties gerado pelo Initializr:
spring.profiles.active=${APP_PROFILE:dev} spring.jpa.open-in-view=false server.port=9000
A expressão ${APP_PROFILE:dev} significa: use a variável de ambiente APP_PROFILE se ela existir; caso contrário, use dev como padrão. Isso permite que em produção você defina APP_PROFILE=prod sem alterar o código.
Crie o arquivo application-dev.properties com as configurações do banco H2 para desenvolvimento:
# Dados de conexao com o banco H2 spring.datasource.url=jdbc:h2:mem:authdb spring.datasource.username=sa spring.datasource.password= # Configuracao do cliente web do banco H2 spring.h2.console.enabled=true spring.h2.console.path=/h2-console # Configuracao para mostrar o SQL no console spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true
O banco H2 in-memory (jdbc:h2:mem:authdb) é recriado a cada reinício, o que é ideal para desenvolvimento. O console web do H2 em /h2-console permite inspecionar o banco via browser enquanto o servidor está rodando.
Criando a classe de configuração do Authorization Server
Toda a configuração do AS ficará em uma única classe: AuthorizationServerConfig. Crie-a no pacote com.devsuperior.authserver.config.
Vamos construir essa classe parte por parte.
@Configuration @EnableWebSecurity public class AuthorizationServerConfig { // ... }
@EnableWebSecurity é necessária para que o Spring Security processe os beans SecurityFilterChain que vamos definir.
1. Filtros do Spring Security
O Spring Security usa uma pilha de filter chains. Cada chain tem um securityMatcher que define para quais URLs ela se aplica. O AS precisa de dois:
Filter chain 1 — endpoints do Authorization Server
Este filter chain intercepta todas as URLs padrão do protocolo OAuth2 e OIDC: /oauth2/authorize, /oauth2/token, /oauth2/jwks, /userinfo, /.well-known/openid-configuration, etc.
@Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); http .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()) .with(authorizationServerConfigurer, as -> as // Habilita suporte a OpenID Connect (ID token, /userinfo, discovery) .oidc(Customizer.withDefaults()) ) .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) // Redireciona para /login quando o browser acessa um endpoint protegido .exceptionHandling(ex -> ex .defaultAuthenticationEntryPointFor( new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML) ) ); return http.build(); }
Alguns pontos importantes:
getEndpointsMatcher()retorna umRequestMatcherque cobre exatamente os endpoints registrados pelo protocolo — não é necessário listá-los manualmente..oidc(Customizer.withDefaults())habilita o suporte a OpenID Connect. Sem isso, o AS funciona apenas como OAuth2 puro, sem emitir ID tokens nem expor o discovery document.- O
LoginUrlAuthenticationEntryPointgarante que quando um browser (que enviaAccept: text/html) acessar um endpoint protegido, ele seja redirecionado para/loginem vez de receber um401JSON.
Filter chain 2 — demais rotas
Este filter chain captura tudo que não foi interceptado pela chain 1: a tela de login, o console H2, e qualquer outra rota que possamos criar no futuro.
@Bean @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/h2-console/**").permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()) .csrf(csrf -> csrf .ignoringRequestMatchers("/h2-console/**") ) .headers(headers -> headers .frameOptions(fo -> fo.sameOrigin()) ); return http.build(); }
.formLogin(Customizer.withDefaults())ativa a tela de login padrão do Spring Security em/login.- O console H2 usa
<iframe>internamente, o que conflita com a proteção padrão deX-Frame-Options. A configuração.frameOptions(fo -> fo.sameOrigin())permite iframes da mesma origem. - O CSRF é desabilitado para
/h2-console/**porque o console H2 não envia o token CSRF.
2. Infraestrutura de usuários
PasswordEncoder
@Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }
O DelegatingPasswordEncoder é o encoder padrão recomendado pelo Spring Security. Ele funciona com prefixos no hash armazenado: {bcrypt}$2a$..., {noop}senha, etc. Isso é fundamental porque o Spring Authorization Server usa este mesmo bean para verificar o client secret do cliente registrado — um BCryptPasswordEncoder puro não consegue processar o prefixo {noop} e causaria erro de autenticação do cliente.
UserDetailsService
Vamos criar o componente UserDetailsService (padrão do Spring Security) para acessar os usuários. Para este primeiro estágio, os usuários são armazenados em memória. Em um post futuro, migraremos para banco de dados.
@Bean public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { UserDetails admin = User.builder() .username("admin@example.com") .password(passwordEncoder.encode("12345678")) .roles("ADMIN") .build(); UserDetails operator = User.builder() .username("operator@example.com") .password(passwordEncoder.encode("12345678")) .roles("OPERATOR") .build(); return new InMemoryUserDetailsManager(admin, operator); }
O método passwordEncoder.encode("12345678") produz um hash no formato {bcrypt}$2a$10$..., que o DelegatingPasswordEncoder sabe verificar no momento do login.
3. Registro de aplicações clientes
No OAuth2, um cliente é uma aplicação que solicita acesso a recursos em nome do usuário. No nosso caso, o cliente é o frontend Next.js.
@Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient nextjsClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("nextjs-client") // {noop} = sem encoding de senha, apenas para desenvolvimento .clientSecret("{noop}nextjs-secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) // URL de callback do Next.js (Auth.js) .redirectUri("http://localhost:3000/api/auth/callback/spring") // URL de callback para testes manuais (browser) .redirectUri("http://localhost:9000/authorized") .postLogoutRedirectUri("http://localhost:3000") // Scopes disponíveis para este cliente .scope(OidcScopes.OPENID) // obrigatório para OIDC / ID token .scope(OidcScopes.PROFILE) // nome, foto — opcional (usuário pode negar) .scope("products:read") // leitura de produtos .scope("products:write") // escrita de produtos // Exige que o usuário aprove os scopes na tela de consentimento .clientSettings(ClientSettings.builder() .requireAuthorizationConsent(true) .build()) .build(); return new InMemoryRegisteredClientRepository(nextjsClient); }
Alguns pontos a destacar:
CLIENT_SECRET_BASICenvia as credenciais do cliente no headerAuthorization: Basic.CLIENT_SECRET_POSTenvia no corpo do form. Ambos são suportados para flexibilidade de teste.AUTHORIZATION_CODEé o único grant type seguro para aplicações com interface de usuário. OREFRESH_TOKENpermite renovar o access token sem novo login.- Os scopes definem o que o cliente pode solicitar.
openideprofilesão scopes OIDC padrão.products:readeproducts:writesão scopes customizados que criaremos no Resource Server. requireAuthorizationConsent(true)ativa a tela de consentimento onde o usuário aprova (ou nega) cada scope individualmente — aquela tela clássica que você já viu ao "Entrar com Google".
4. Infraestrutura JWT
JWKSource - para acessar o par de chaves RSA
Os JWTs emitidos pelo AS são assinados com uma chave privada RSA e verificados com a chave pública correspondente. O Resource Server busca a chave pública via endpoint /oauth2/jwks e valida os tokens localmente — sem precisar chamar o AS a cada requisição.
@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(); return new ImmutableJWKSet<>(new JWKSet(rsaKey)); } private static KeyPair generateRsaKey() { try { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(2048); return generator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } }
Atenção: O par de chaves gerado aqui é efêmero — é criado em memória a cada inicialização do servidor. Isso significa que todos os tokens emitidos anteriormente se tornam inválidos após um restart. Em produção, as chaves devem ser persistidas externamente (ex: AWS KMS, HashiCorp Vault, ou um keystore em disco). Isso será abordado em um post futuro.
JwtDecoder - para validar o acceess token
@Bean public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); }
Este JwtDecoder é usado internamente pelo próprio AS — por exemplo, para validar o access token quando o cliente chama o endpoint /userinfo ou /oauth2/introspect. Ele não é o mesmo JwtDecoder que será configurado no Resource Server.
5. Configuração do servidor
@Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder() .issuer("http://localhost:9000") .build(); }
O issuer é a URL base do AS. Ela aparece na claim iss de todos os tokens emitidos e no discovery document. O Resource Server usará essa URL para descobrir automaticamente os endpoints do AS via /.well-known/openid-configuration.
Testando o servidor
Execute o projeto e verifique os seguintes endpoints no browser:
Discovery document OIDC — http://localhost:9000/.well-known/openid-configuration
Retorna um JSON com todos os endpoints do AS, algoritmos suportados, scopes, etc. Este documento é consumido automaticamente pelo Resource Server e pelo cliente Next.js para auto-configuração.
{ "issuer": "http://localhost:9000", "authorization_endpoint": "http://localhost:9000/oauth2/authorize", "token_endpoint": "http://localhost:9000/oauth2/token", "jwks_uri": "http://localhost:9000/oauth2/jwks", "userinfo_endpoint": "http://localhost:9000/userinfo", ... }
JWK Set — http://localhost:9000/oauth2/jwks
Expõe as chaves públicas RSA em formato JSON. O Resource Server busca este endpoint uma vez e faz cache da chave para validar os JWTs localmente.
Tela de login — http://localhost:9000/login
A tela de login padrão do Spring Security. Futuramente será substituída por uma tela customizada.
Testando no Postman
Por favor acesse a pasta do projeto no Github e baixe a collection Postman, que é o arquivo authserver.postman_collection.json localizado na pasta acima.
Importe a collection no seu Postman. Depois de importar a collection no seu Postman, navegue nela para a pasta 1. Discovery & JWK e teste os endpoints:
OIDC Discovery DocumentOAuth2 AS MetadataJWK Set (chave pública RSA)
Testando o fluxo Authorization Code
Cole esta URL no browser para iniciar o fluxo completo:
http://localhost:9000/oauth2/authorize?response_type=code&client_id=nextjs-client&redirect_uri=http://localhost:9000/authorized&scope=openid%20profile%20products:read%20products:write&state=xyz&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256
O par PKCE utilizado neste exemplo de teste é fixo para conveniência:
code_verifier:dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkcode_challenge:E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM(= BASE64URL(SHA256(verifier)))
Após o login e a aprovação dos scopes, o browser redirecionará para http://localhost:9000/authorized?code=XXXX&state=xyz. O endpoint /authorized não existe, então a página dará 404 — isso é esperado. Copie o code da URL.
Em seguida, copie o valor do argumento o code no resultado acima na barra de URL do seu navegador, depois acesse a collection -> Variables, e copie o valor para o parâmetro authorization_code.
Depois de copiar o valor de authorization_code, acesse a pasta 2. Fluxo Authorization Code da collection e execute a requisição:
[2] Trocar code por tokens
A resposta conterá access_token, refresh_token e id_token. Cole o access_token em jwt.io para inspecionar o payload.
Código completo para baixar
O código completo deste post está disponível no Github do blog.
Conclusão
Construímos um Authorization Server mínimo e funcional com Spring Boot 4 e Spring Authorization Server 7. Com apenas uma classe de configuração e dois arquivos de properties, temos:
- Um servidor OAuth2/OIDC completo rodando na porta 9000
- Suporte a Authorization Code com PKCE (o único fluxo recomendado pelo OAuth 2.1 para aplicações com usuário)
- Tela de consentimento de scopes
- Emissão de JWTs assinados com RS256
- Discovery document OIDC totalmente funcional
A implementação in-memory é intencional neste ponto — ela mantém o código mínimo e focado no que importa: entender a estrutura e o fluxo do protocolo.
Em estudos futuros, faremos incrementos na direção de se construir uma aplicação completa de referência para os principais recursos para autenticação e autorização com OAuth2 e OpenID Connect (OIDC).

