Integrando o NextAuth.js com o Spring Authorization Server

Configure uma aplicação Next.js para autenticar e autorizar usuários com um servidor Spring de OAuth 2

17/01/2025

Integrando o NextAuth.js com o Spring Authorization Server

Este é um post rápido para demonstrar como adicionar autenticação de usuário ao seu aplicativo web usando OAuth 2 com NextAuth.js e um servidor de autorização Spring. Como há pouca informação disponível online sobre a integração dessas duas tecnologias, vou prover aqui um guia passo a passo. O processo é bastante simples, mas há alguns detalhes para os quais você deve estar atento.

Aplicação de demonstração usando Next.js 15

O código para esta simples aplicação web pode ser encontrado aqui. Ela consiste em uma página inicial contendo apenas um botão de Sign in. O usuário pode então se autenticar com sua conta do Github ou com o nosso servidor de OAuth 2 customizado, do qual iremos tratar na próxima seção.

Application

Após autenticado e autorizado, o usuário é redirecionado à página principal, que agora apresenta uma mensagem de boas-vindas e um link de logout. O menu superior também exibe um link para uma página de profile, onde algumas informações do usuário conectado são exibidas.

A configuração do NextAuth.js reside no arquivo src/auth/auth-options.ts, onde o provider padrão do Github e nosso provider OAuth personalizado são definidos e adicionados ao objeto authOptions exportado. O provider personalizado deve ser configurado para utilizar a versão 2 do OAuth e os endpoints expostos pelo nosso servidor de autorização. O escopo deve ser definido como openid para que o aplicativo cliente tenha acesso às informações básicas de perfil do usuário autenticado.

import GithubProvider from "next-auth/providers/github";
 
const githubProvider = GithubProvider({
  clientId: process.env.GITHUB_ID!,
  clientSecret: process.env.GITHUB_SECRET!,
});
 
const springOAuthProvider = {
  id: "spring",
  name: "Spring Auth Server",
  type: "oauth",
  version: "2",
  authorization: "http://localhost:9000/oauth2/authorize",
  token: "http://localhost:9000/oauth2/token",
  userinfo: "http://localhost:9000/userinfo",
  clientId: process.env.SPRING_CLIENT_ID,
  clientSecret: process.env.SPRING_CLIENT_SECRET,
  clientAuthMethod: "client_secret_basic",
  redirectUri: "http://localhost:3000/api/auth/callback",
  scope: "openid",
  idToken: true,
  issuer: "http://localhost:9000",
  // jwks_endpoint: 'http://localhost:9000/oauth2/jwks',
  wellKnown: "http://localhost:9000/.well-known/openid-configuration",
  profile: (profile: any) => {
    console.log("profile", profile);
    return {
      id: profile.user.id.toString(),
      name: profile.user.username,
      email: profile.user.email,
    };
  },
};
 
export const authOptions = {
  providers: [githubProvider, springOAuthProvider],
};

Muito embora o endpoint /.well-known/openid-configuration seja padrão em muitas soluções de autenticação, aqui tivemos que explicitá-lo na configuração do provider para que não houvesse erros durante o processo de autorização do usuário.

As entradas clientId e clientSecret serão carregadas das variáveis de ambiente SPRING_CLIENT_ID e SPRING_CLIENT_SECRET, respectivamente. Leia o arquivo README.md do projeto para verificar como configurá-las corretamente.

Configurando o NextAuth.js

Como estamos usando o Next.js 15 com o app router neste projeto, devemos exportar um objeto NextAuth como um handler para chamadas de API no arquivo app/api/auth/[...nextauth]/route.ts, conforme descrito na documentação oficial.

import { authOptions } from "@/auth/auth-options";
import NextAuth from "next-auth";
 
const handler = NextAuth(authOptions);
 
export { handler as GET, handler as POST };

Finalmente, devemos criar um arquivo middleware.ts e colocá-lo na raiz da pasta src para podermos definir as rotas protegidas do nosso aplicativo. No nosso caso, a única página protegida será /profile.

export { default } from "next-auth/middleware";
 
// aplica o next-auth apenas para as rotas definidas
export const config = { matcher: ["/profile"] };

Spring Authorization Server

O código para o nosso servidor de autorização personalizado pode ser encontrado aqui. É basicamente um dos aplicativos de exemplo da documentação oficial do Spring Authorization Server com algumas modificações e suporte para OpenId Connect.

Primeiramente, vamos configurar a porta do servidor e os parâmetros do cliente no arquivo application.yml, supondo que o aplicativo cliente será executado em http://localhost:3000.

server:
  port: 9000
 
logging:
  level:
    org.springframework.security: trace
 
app:
  auth:
    client:
      id: oidc-client
      secret: secret
      redirect-uri: "http://localhost:3000/api/auth/callback/spring"
      logout-redirect-uri: "http://localhost:3000/"

Observe o parâmetro redirect-uri. O token final é usado para identificar o provedor OAuth no NextAuth.js, portanto, deve ser igual ao id do provedor personalizado springOAuthProvider definido na seção anterior. O id e o client secret também devem corresponder exatamente àqueles definidos no arquivo de configuração do NextAuth.js. Esses parâmetros são usados no método do bean registeredClientRepository em SecurityConfig, como mostrado abaixo:

// SecurityConfig.java
 
...
@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
  // This is just a properties bean that holds the client parameters
  private final ClientConfig clientConfig;
 
  ...
 
  @Bean
  public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient oidcClient = RegisteredClient.withId(randomUUID().toString())
        .clientId(clientConfig.id())
        .clientSecret("{noop}" + clientConfig.secret())
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .redirectUri(clientConfig.redirectUri())
        .postLogoutRedirectUri(clientConfig.logoutRedirectUri())
        .scope(OidcScopes.OPENID)
        .scope(OidcScopes.PROFILE)
        .build();
 
    return new InMemoryRegisteredClientRepository(oidcClient);
  }
 
...
}

Para este aplicativo de demonstração, estamos definindo manualmente um único usuário com nome de usuário user e senha password, assim como no exemplo da documentação oficial. Para adicionar informações de perfil do usuário autenticado ao token JWT, também devemos implementar um método construção do bean OAuth2TokenCustomizer:

// SecurityConfig.java
 
...
@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
...
 
  @Bean
  public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer(OidcUserInfoService userInfoService) {
    return (context) -> {
      if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
        OidcUserInfo userInfo = userInfoService.loadUser(context.getPrincipal().getName());
 
        context.getClaims().claims(claims -> claims.putAll(userInfo.getClaims()));
      }
    };
  }
 
...
}

Para recuperar as informações do usuário a serem incorporadas no token, definimos uma classe chamada OidcUserInfoService. Essa classe fornece o método loadUser, que recebe um nome de usuário e retorna um objeto OidcUserInfo contendo os dados de perfil. As informações do usuário são recuperadas usando um repositório incorporado na classe de service.

O UserInfoRepository serve como uma fonte de dados simples, fornecendo as informações de perfil do usuário em um Map. O repositório é inicializado com o único usuário identificado por user. O método findByUsername retorna os dados de perfil que correspondem ao nome de usuário fornecido. Aqui está o código que implementa esse processo:

@Service
public class OidcUserInfoService {
 
  private final UserInfoRepository userInfoRepository = new UserInfoRepository();
 
  public OidcUserInfo loadUser(String username) {
    return new OidcUserInfo(this.userInfoRepository.findByUsername(username));
  }
 
  static class UserInfoRepository {
 
    private final Map<String, Map<String, Object>> userInfo = new HashMap<>();
 
    public UserInfoRepository() {
      this.userInfo.put("user", createUser());
    }
 
    public Map<String, Object> findByUsername(String username) {
      return this.userInfo.get(username);
    }
 
    private static Map<String, Object> createUser() {
      return Map.of(
          "user", Map.of(
                  "id", 1,
                  "username", "user",
                  "name", "User",
                  "email", "user@email.com"
          )
      );
    }
  }
}

Executando a stack

Os processos de execução de cada aplicativo são descritos nos arquivos README localizados em seus respectivos repositórios. Uma vez que ambos os aplicativos (servidor de autenticação Spring e aplicativo Next.js) estejam executando, você pode acessar a interface web navegando para http://localhost:3000 no seu navegador.

Clique no botão Sign in e, em seguida, "Sign in with Spring Auth Server". O navegador deve exibir a página de login do servidor de autenticação. Você pode verificar a guia de rede das ferramentas de desenvolvimento do navegador para verificar que o endpoint authorize foi chamado.

Página de login

Após inserir as credenciais de usuário user e password, você deve ser redirecionado à página principal. Agora a barra de navegação superior deve exibir um link para a página de perfil e um botão para sair. Ao clicar no link profile, o aplicativo deve exibir algumas informações sobre o usuário conectado, como mostrado na figura abaixo:

Página de perfil

O link Logout deve redirecioná-lo à página de sign-out padrão do NextAuth.js, solicitando que você confirme se realmente deseja encerrar sua sessão. Após confirmar, você será redirecionado para a página inicial com a sessão finalizada.

E é isso...

Neste artigo desenvolvemos um setup de autenticação funcionando com um servidor de autorização Spring e um aplicativo Next.js usando NextAuth.js. Isso permite que os usuários façam login de forma segura e acessem páginas protegidas dentro da sua aplicação. Você pode usar esse projeto como ponto de partida para adicionar mais recursos ou personalizar o fluxo de autenticação conforme necessário.