Authentication with?OpenID
From parent article
Motivation
We want to provide users with the option to sign into their account using OpenID, specifically through Google Login. The motivation behind this is to offer a convenient alternative to remembering or signing in with a password. If a user forgets their password, they can easily sign in using their Google account. Additionally, if the user is already signed into their Google account on their browser, signing into our application becomes a one-click process.
What is?OpenID?
OAuth 2.0 is an authorization protocol that allows our application to access a user’s resources through a third party without exposing their credentials.
?OpenID Connect (OIDC) is an authentication protocol that builds on OAuth 2.0; it helps us identify the user by returning user-specific information from the third party.
In this case, when a user wants to sign into our application without providing their credentials, we redirect them to Google Login. Upon successful authentication, Google returns the user’s email, which serves as a unique identifier that we have stored in our database. This allows us to authenticate the user securely without needing their credentials.
OpenID Flow
Setting Up?OpenID
2. Create OAuth consent screen, on the left sidebar go to OAuth Consent Screen. Fill in: - App name - User support email - Upload App logo - Add App domain name - Add Authorized Domain - Developer contact information email
3. Navigate to APIs & Services > Credentials. Create Credentials
4. Go to https://dashboard.render.com/ Add the following environment variables - GOOGLE_CLIENT_ID: - GOOGLE_CLIENT_SECRET: - GOOGLE_REDIRECT_BASE_URI:
领英推荐
Setting Up the Application
Add the following dependency to pom.xml.
<!-- Spring Security OAuth2 Client -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.37.2</version> <!-- Replace with the fixed version -->
</dependency?
public class CustomOAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Value("${OAUTH2_REDIRECT_BASE_URI}")
private String baseRedirectUri;
private String redirectUri;
private String failureRedirectUri;
@Autowired
UserService userService;
@Autowired
JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
this.redirectUri = baseRedirectUri + "/oauth2/success";
this.failureRedirectUri = baseRedirectUri + "/oauth2/failure";
handle(request, response, authentication);
super.clearAuthenticationAttributes(request);
}
/**
* We are close to end of the OAuth2 flow, after the Client redirected the user to the OAuth2 provider,
* after the user sends its credential/consent to the OAuth2 provider, the OAuth2 provider redirects the user back to the Client.
* providing the authorization code/token to the Client to retrieve the users' information.
* This method handles the successfull retrieval of the user's information from the OAuth2 provider.
* Where we validate that the user's email matches the email in the database, and using the user's information
* we generate the JWT access/refresh token, in which we redirect to the /oauth2/success endpoint otherwise
* we redirect to the /oauth2/failure endpoint.
*/
@Override
protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication oauthAuthentication) throws IOException {
String targetUrl = redirectUri.isEmpty() ?
determineTargetUrl(request, response, oauthAuthentication) : redirectUri;
OAuth2AuthenticationToken oauth2Authentication = (OAuth2AuthenticationToken) oauthAuthentication;
try {
if (!Boolean.TRUE.equals(oauth2Authentication.getPrincipal().getAttribute("email_verified"))) {
targetUrl = UriComponentsBuilder.fromUriString(failureRedirectUri)
.queryParam("error", "Email not verified")
.build().toUriString();
response.setContentType(CONTENT_TYPE);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
return;
}
String email = oauth2Authentication.getPrincipal().getAttribute("email");
Optional<User> optionalUser = userService.getByEmail(email);
if(optionalUser.isEmpty()) {
String failureUrl = UriComponentsBuilder.fromUriString(failureRedirectUri)
.queryParam("error", "User not found with email: " + email)
.build().toUriString();
response.setContentType("application/json");
getRedirectStrategy().sendRedirect(request, response, failureUrl);
return;
}
User user = optionalUser.get();
UsernameEmailPasswordAuthenticationToken authentication = new UsernameEmailPasswordAuthenticationToken(user.getId(), user.getUsername(), user.getAuthorities());
String accessToken = jwtUtils.generateToken(authentication, TokenType.ACCESS_TOKEN);
String refreshToken = jwtUtils.generateToken(authentication, TokenType.REFRESH_TOKEN);
userService.updateTokens(authentication.getId(), accessToken, refreshToken);
// Set refresh token in HTTP-only cookie
Cookie refreshTokenCookie = getRefreshTokenCookie(refreshToken);
response.addCookie(refreshTokenCookie);
targetUrl = UriComponentsBuilder.fromUriString(targetUrl)
.queryParam("accessToken", accessToken)
.build().toUriString();
response.setContentType(CONTENT_TYPE);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
} catch (AuthenticationException e) {
targetUrl = UriComponentsBuilder.fromUriString(failureRedirectUri)
.queryParam("error", e.getMessage())
.build().toUriString();
response.setContentType(CONTENT_TYPE);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
private Cookie getRefreshTokenCookie(String refreshToken) {
Cookie refreshTokenCookie = new Cookie("refresh-token", refreshToken);
refreshTokenCookie.setHttpOnly(true); // prevents JavaScript from accessing the cookie
refreshTokenCookie.setSecure(true); // should be set to true in production
refreshTokenCookie.setPath("/");
refreshTokenCookie.setMaxAge((int) JwtUtils.REFRESH_TOKEN_EXPIRATION_TIME.toSeconds()); // Evicts the cookie from browser when the token expires
return refreshTokenCookie;
}
}
Upon successful authentication, it will create an access and refresh token, redirect the user to the “/oauth/success” endpoint where the tokens are returned to the user. If the authentication fails, redirect user to “/oauth2/failure”.
Now update SecurityConfig.java
org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE;
import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
public static final String[] WHITE_LIST_URL = {
"/public/**",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
"/login/oauth2/**"
};
@Bean
public ClientRegistrationRepository clientRegistrationRepository(
@Value("${GOOGLE_CLIENT_ID}") String googleClientId,
@Value("${GOOGLE_CLIENT_SECRET}") String googleClientSecret,
@Value("${OAUTH2_REDIRECT_BASE_URI}") String baseRedirectUri) {
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("google")
.clientId(googleClientId)
.clientSecret(googleClientSecret)
.clientAuthenticationMethod(CLIENT_SECRET_BASIC)
.authorizationGrantType(AUTHORIZATION_CODE)
.redirectUri(baseRedirectUri + "/login/oauth2/code/google")
.scope("openid", "profile", "email")
.authorizationUri("https://accounts.google.com/o/oauth2/auth")
.tokenUri("https://oauth2.googleapis.com/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.userNameAttributeName("sub")
.clientName("Google")
.build();
return new InMemoryClientRegistrationRepository(clientRegistration);
}
@Bean
public CustomOAuth2SuccessHandler customAuthenticationSuccessHandler() {
return new CustomOAuth2SuccessHandler();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
JwtAuthenticationFilter jwtAuthenticationFilter,
UsernameEmailPasswordAuthenticationFilter usernameEmailPasswordAuthenticationFilter,
ClientRegistrationRepository clientRegistrationRepository,
CustomOAuth2SuccessHandler customAuthenticationSuccessHandler) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Disable CSRF protection
.authorizeHttpRequests(auth -> auth
.requestMatchers(WHITE_LIST_URL).permitAll() // Whitelist signup endpoint
.anyRequest().authenticated() // All other endpoints require authentication
)
/**
* Because we are using JWT(self contain) tokens, we can make the api calls stateless, which means
* we don't need to store the session in the server. But for OAuth2 flows, because we need to make multiple
* calls between the client(our application), user, and the google authorization server, to exchange user
* credentials for oauth2 tokens, and exchange the tokens for user information to authenticate the user.
* We need to use sessions stored on the client to keep track of the keys and personal information that
* is propagated during the sequence of api calls.
* So for the sequence of api calls for OAuth2 authentication we need the REST calls to be stateful, while
* for the rest of the api calls we can make them stateless.
*/
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Default to stateless
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // Stateful for OAuth2 flows
.sessionFixation().none() // No session fixation protection
)
.oauth2Login(oauth2 ->
oauth2.clientRegistrationRepository(clientRegistrationRepository)// Ensure OAuth2 login is configured
.successHandler(customAuthenticationSuccessHandler))
.authenticationProvider(authenticationProvider()) // Register custom authentication provider
.addFilterAt(usernameEmailPasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
We want our application to be stateless for most endpoints, but maintain a session for the OAuth 2.0 endpoints because the token exchange and retrieval of user information are done through URL redirections. If we don’t maintain a session where the user information is temporarily stored on the server, we would lose crucial data during the redirections, preventing us from properly authenticate the user.
Let’s create OAuthController.java, where the /oauth2/success and /oauth2/failure endpoints handle the return of the JWT to the user at the end of the OAuth2 flow. The /oauth2/success endpoint is responsible for returning the JWT upon successful authentication.
@RestController
public class OAuthController {
@GetMapping("/oauth2/success")
public ResponseEntity<?> success(@RequestParam String accessToken, HttpServletResponse response) {
/**
* Since the api calls is stateful during the OAuth2 flow, and this endpoint marks the end of the OAuth2 flow,
* we remove the JSESSIONID cookie to revert back to stateless api calls.
*/
response.addCookie(removeJSessionIdCookie());
try {
return ResponseEntity.ok(new AccessTokenResponse(accessToken, ACCESS_TOKEN_EXPIRATION_TIME.toSeconds()));
} catch(Exception e) {
return ResponseEntity.badRequest().body("Error: " + e.getMessage());
}
}
@GetMapping("/oauth2/failure")
public ResponseEntity<?> failure(@RequestParam String error, HttpServletResponse response) {
/**
* Since the api calls is stateful during the OAuth2 flow, and this endpoint marks the end of the OAuth2 flow,
* we remove the JSESSIONID cookie to revert back to stateless api calls.
*/
response.addCookie(removeJSessionIdCookie());
return ResponseEntity.badRequest().body(error);
}
private Cookie removeJSessionIdCookie() {
Cookie jsessionidCookie = new Cookie("JSESSIONID", null);
jsessionidCookie.setPath("/");
jsessionidCookie.setMaxAge(0);
return jsessionidCookie;
}
}
Since the success and failure endpoints mark the end of the OAuth 2.0 flow, we want to revert the application to a stateless configuration. State is managed through the JSESSIONID cookie during the OAuth 2.0 process, so after completion, we ensure the application remains stateless by removing the stored JSESSIONID cookie from the client side.
Signing in with?Google
To sign in with Google, go to the following URL
https://<application url>/oauth2/authorization/google
It will return the JWT access token in the response body and the refresh token in the httpOnly cookie named refresh-token.