Integrating NextAuth.js with Spring Authorization Server
Setup a Next.js application to authenticate and authorize users with a Spring OAuth 2 server
This is a quick post to show you how to add user authentication to your web application using OAuth 2 with NextAuth.js and a Spring authorization server. Since there's limited information available online about integrating these two technologies, I'll walk you through the process step-by-step. The whole thing is pretty straightforward, but there are a few details you must be aware of.
Next.js 15 demo application
The code for this simple web application can be found here. It consists of a main page with a Sign in link. The user can either authenticate with their Github account or with our custom OAuth 2 server (which will be covered in the next section).
After authenticated and authorized, the user is redirected to the main page, which now presents a welcome message and a logout link. The top menu also exhibits a link to a profile page, where some of the logged-in user information is displayed.
The NextAuth.js configuration resides in the src/auth/auth-options.ts
file, where the standard Github provider and our custom OAuth provider are defined and added to the exported authOptions
object. The custom provider must be configured to use OAuth version 2 and the endpoints exposed by our authorization server. The scope must be set to openid
so the client application can have access to basic profile information of the authenticated user.
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],
};
The clientId
and clientSecret
entries will be loaded from the environment variables SPRING_CLIENT_ID
and SPRING_CLIENT_SECRET
respectively. Read the project's README.md file to check how to properly set them up.
Configuring NextAuth.js
Since we are using Next.js 15 with the app router in this project, we must export a NextAuth
object as a handler for API calls in the file app/api/auth/[...nextauth]/route.ts
, as described in the official documentation.
import { authOptions } from "@/auth/auth-options";
import NextAuth from "next-auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Finally, we must create a middleware.ts
file and put it in the root of the src
folder to be able to define the protected routes of our application. In our case, the only protected route will be /profile
.
export { default } from "next-auth/middleware";
// applies next-auth only to matching routes
export const config = { matcher: ["/profile"] };
Spring Authorization Server
The code for our custom authorization server can be found here. It is basically one of the sample apps from the Spring Authorization Server official documentation with a few modifications and support for OpenId Connect.
First, let's configure the server port and the client parameters in the application.yml
file, assuming that the client application will run on 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/"
Notice the redirect-uri
parameter. The final token is used to identify the OAuth provider in NextAuth.js, so it should be equal to the id
of the custom provider springOAuthProvider
defined in the previous section. The client's id and secret should also match perfectly with the ones configured in the NextAuth.js configuration file. These parameters are used in the registeredClientRepository
bean method in the SecurityConfig
as shown below:
// 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);
}
...
}
For this demo application, we are manually defining a single user of username user
and password password
, just like in the official documentation example. In order to add profile information of the authenticated user to the JWT token, we must also implement a OAuth2TokenCustomizer
bean method:
// 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()));
}
};
}
...
}
To retrieve user information to be embedded in the token, we define a class called OidcUserInfoService
. This class provides the method loadUser
, which takes a username and returns an OidcUserInfo
object containing the profile data of the user. The user information is fetched using a repository embedded within the service class.
The UserInfoRepository
serves as a simple data source, providing the user profile information in a Map. The repository is initialized with the single user of username user
, and the findByUsername
method retrieves the relevant data that corresponds to the provided username. Here's the code that implements this process:
@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"
)
);
}
}
}
Running the stack
The processes of running each application are described in the README files located in their respective repositories. Once both applications (Spring auth server and Next.js app) are up and running, you can access the web interface by navigating to http://localhost:3000
in your browser.
Click the Sign in button and then "Sign in with Spring Auth Server". The browser should display the authorization server's login page. You can check the network tab of your browser's dev tools to verify that the authorize
endpoint has been requested.
After entering the user's credentials user
and password
, you should be redirected to the main page and the top navigation bar should now display a link to the profile page and a button for logging out. By clicking the profile link, the app should display some information about the logged-in user as shown in the figure below:
The Logout link should redirect you to the default NextAuth.js sign-out page, prompting you to confirm whether you truly wish to end your session. After confirming, you will be redirected to the homepage, at which point your session will be terminated.
That's it
In conclusion, you should now have a working authentication setup with a Spring authorization server and a Next.js app using NextAuth.js. This allows users to log in securely and access protected pages within your application. You can use this as a starting point to add more features or customize the authentication flow as needed.