Java
Sign in with NIA — Spring Boot
Add “Sign in with NIA” to a Spring Boot app with Spring Security OAuth2 Client and the standard OpenID Connect flow. Follow the steps top to bottom — tick each one off as you go.
01Overview
This guide adds “Sign in with NIA” to a Spring Boot application using the standard OpenID Connect flow. NIA is a conformant OIDC provider, so you use Spring Security OAuth2 Client — Spring's built-in OAuth/OIDC client — with no NIA-specific SDK.
Spring Boot runs as a confidential client: the browser is redirected to NIA to sign in, and your Spring server exchanges the returned code for tokens using your client secret. Spring Security handles PKCE, state, and ID-token signature validation for you.
- Flow: Authorization Code + PKCE (S256)
- Tokens: RS256 access & ID tokens, opaque refresh token
- You write: a security config and (optionally) a controller that reads the principal
02Requirements
- Java 17+ and Spring Boot 3+
- A NIA account with access to the client dashboard
- A registered OAuth client (
client_id+client_secret) - Your app's redirect URI registered on that client
- Maven or Gradle build tooling
03Installation
Add the spring-boot-starter-oauth2-client dependency. It pulls in Spring Security and the OAuth2/OIDC client support used throughout this guide.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</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-web</artifactId>
</dependency>04Required OAuth library
Register NIA once in application.yml. The issuer-uri points at NIA's discovery document — every endpoint, the JWKS URL, and supported algorithms are configured automatically from it, and PKCE is enabled automatically for this registration.
spring:
security:
oauth2:
client:
registration:
nia:
client-id: ${NIA_CLIENT_ID}
client-secret: ${NIA_CLIENT_SECRET}
scope: openid,email,profile
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/nia"
provider:
nia:
issuer-uri: https://dev-api.niaauth.comoffline_access to scope only if you need refresh tokens (see step 14).05Environment Variables
Never hard-code secrets. The issuer already lives in application.yml — only the client credentials need to come from the environment.
NIA_CLIENT_ID=your-client-id
NIA_CLIENT_SECRET=your-client-secretspring-dotenv / your platform's secret manager) — Spring reads ${NIA_CLIENT_ID} placeholders from the process environment at startup.06Register OAuth Client
In the NIA dashboard, create the client your Spring Boot app will authenticate as:
- Open Dashboard → Clients → New client
- Client type: Web (confidential)
- Allowed scopes:
openidemailprofile(addoffline_accessfor refresh) - Add your redirect URI (next step)
- Copy the
client_idandclient_secret— the secret is shown once
07Redirect URI
NIA matches redirect URIs exactly — scheme, host, port, and path must be identical, with no trailing-slash difference. Spring's default redirect template is {baseUrl}/login/oauth2/code/{registrationId}, which resolves to /login/oauth2/code/nia for this registration. Register one URI per environment.
# Development
http://localhost:8080/login/oauth2/code/nia
# Production
https://app.example.com/login/oauth2/code/nia/login/oauth2/code/nia — Spring will not process the callback on any other path for this registration.08Issuer
The issuer is the single URL every OIDC library needs. Its discovery document advertises all endpoints below.
Issuer: https://dev-api.niaauth.com
Discovery: https://dev-api.niaauth.com/.well-known/openid-configuration
Authorization: https://dev-api.niaauth.com/oauth/authorize
Token: https://dev-api.niaauth.com/oauth/token
UserInfo: https://dev-api.niaauth.com/oauth/userinfo
JWKS: https://dev-api.niaauth.com/oauth/jwks.json09Authorization Flow
Add a security config that requires authentication and enables OAuth2 login. Spring redirects unauthenticated users to NIA automatically — you do not write a login view.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults());
return http.build();
}
}10Token Exchange
Spring Security performs the authorization code exchange — including sending the PKCE verifier — at /login/oauth2/code/nia automatically. You don't write this step; it happens inside the filter chain configured above.
The resulting tokens are stored in an OAuth2AuthorizedClient that you can retrieve later (see Session Management and Refresh Tokens).
11JWKS Validation
NIA signs ID tokens with RS256. Spring Security fetches the public keys from the JWKS endpoint (discovered automatically from issuer-uri), caches them, and verifies the signature, iss, aud, exp, and nonce during login. You do not verify manually.
To confirm what was validated, inspect the parsed claims from the principal:
@GetMapping("/whoami")
public Map<String, Object> whoami(@AuthenticationPrincipal OidcUser principal) {
Map<String, Object> claims = principal.getClaims();
// signature (RS256, via JWKS), iss, aud, exp and nonce are already
// verified by Spring Security before this controller method runs.
return claims;
}kid, rotation needs no code change on your side.12Session Management
Once login succeeds, Spring stores the authenticated user in the HttpSession. Read it with @AuthenticationPrincipal OidcUser in any controller:
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProfileController {
@GetMapping("/api/me")
public Map<String, Object> me(@AuthenticationPrincipal OidcUser principal) {
return Map.of(
"sub", principal.getSubject(),
"email", principal.getEmail(),
"name", principal.getFullName(),
"claims", principal.getClaims()
);
}
}13Refresh Tokens
If you requested offline_access, NIA returns a refresh token, which Spring Security stores alongside the access token in the OAuth2AuthorizedClient. Use an OAuth2AuthorizedClientManager (backed by a refresh-token provider) so tokens are refreshed automatically when you call NIA-protected resources with WebClient.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.*;
import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clients,
OAuth2AuthorizedClientRepository authorizedClients) {
OAuth2AuthorizedClientProvider provider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken() // auto-refreshes using the stored refresh token
.build();
DefaultOAuth2AuthorizedClientManager manager =
new DefaultOAuth2AuthorizedClientManager(clients, authorizedClients);
manager.setAuthorizedClientProvider(provider);
return manager;
}
@Bean
WebClient niaWebClient(OAuth2AuthorizedClientManager manager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(manager);
oauth2.setDefaultClientRegistrationId("nia");
return WebClient.builder().apply(oauth2.oauth2Configuration()).build();
}
}OAuth2AuthorizedClientRepository.14Logout
Clear the local session and send the user to NIA's end-session endpoint so they are signed out of NIA too, using OidcClientInitiatedLogoutSuccessHandler. Register your post_logout_redirect_uri on the client.
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
@Bean
SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository clients) throws Exception {
OidcClientInitiatedLogoutSuccessHandler logoutHandler =
new OidcClientInitiatedLogoutSuccessHandler(clients);
logoutHandler.setPostLogoutRedirectUri("{baseUrl}/");
http.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults())
.logout(l -> l.logoutSuccessHandler(logoutHandler));
return http.build();
}15Deployment
Before going live:
- Register the production redirect URI (HTTPS) on the client
- Set
NIA_CLIENT_ID/NIA_CLIENT_SECRETas real env vars on your host — never commit them - Serve over HTTPS end-to-end so
{baseUrl}resolves tohttps:// - If behind a reverse proxy or load balancer, set
server.forward-headers-strategyso Spring builds the correct redirect URI - Use a shared/persistent session store (e.g. Redis) behind multiple instances
server:
forward-headers-strategy: framework
spring:
security:
oauth2:
client:
registration:
nia:
redirect-uri: "{baseUrl}/login/oauth2/code/nia"16Troubleshooting
- redirect_uri_mismatch — the path must be exactly
/login/oauth2/code/nia(scheme/host/port must match the registered URI too). - invalid_client — wrong
NIA_CLIENT_SECRET, or the client's token-auth method differs from what Spring sends. - issuer mismatch / discovery failure —
issuer-urimust be exactlyhttps://dev-api.niaauth.com, with no trailing slash. - redirect URI built as http:// behind a proxy — set
server.forward-headers-strategy: framework(ornative) so{baseUrl}reflectsX-Forwarded-*headers. - invalid_scope — the scope isn't in the client's
allowed_scopes; add it in the dashboard. - ID-token signature — usually server clock skew; sync time via NTP.