User must be authenticated with Spring Security before authorization can be completed

Posted by duk on Sat, 25 Dec 2021 17:13:16 +0100

Problem description

When using Spring cloud oauth2 to implement Oauth 2 permission control, call / oauth/authorize to obtain the authorization code, and throw the User must be authenticated with Spring Security before authorization can be completed exception?

Request interface:

The console exception information is:

 

The source code of the interface part is:

@RequestMapping(value = "/oauth/authorize")
	public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
			SessionStatus sessionStatus, Principal principal) {

		// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
		// query off of the authorization request instead of referring back to the parameters map. The contents of the
		// parameters map will be stored without change in the AuthorizationRequest object once it is created.
		AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);

		Set<String> responseTypes = authorizationRequest.getResponseTypes();

		if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
			throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
		}

		if (authorizationRequest.getClientId() == null) {
			throw new InvalidClientException("A client id must be provided");
		}

		try {

			if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
				throw new InsufficientAuthenticationException(
						"User must be authenticated with Spring Security before authorization can be completed.");
			}
        ...

		}
		catch (RuntimeException e) {
			sessionStatus.setComplete();
			throw e;
		}

	}

In the actual debug, we can see that the authorization information is empty, resulting in an InsufficientAuthenticationException thrown.

Delve into the reasons

1. I checked the information on the Internet. One argument is that / oauth/authorize needs to be configured to allow permitAll in the http configuration of security

 protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin().loginPage("/login").and()
                .logout().addLogoutHandler(sysLogoutHandler).logoutSuccessHandler(sysLogoutSuccessHandler)
                .and()
                // Since JWT is used, we don't need csrf here
                .csrf().disable().cors().and()
                .authorizeRequests()
                .antMatchers("/login", "/oauth/authorize").permitAll()
                // All requests except the above require authentication
                .anyRequest().authenticated();

    }

After configuration, I still report the same error when I request the interface again, indicating that this error is not caused by configuration.

After a long attempt, I saw the error message on the console.

The code of GlobalExceptionTranslator is as follows:

@RestControllerAdvice
@Slf4j
public class GlobalExceptionTranslator {
    @ExceptionHandler(InsufficientAuthenticationException.class)
    public BaseResponse<Object> handleError(InsufficientAuthenticationException e) {
        log.error("Permission Denied", e);
        return BaseResponse
                .builder()
                .code(ResultCode.INTERNAL_SERVER_ERROR.getCode())
                .msg(e.getMessage())
                .data(null)
                .build();
    }
}

Originally, my problem is that I caught the InsufficientAuthenticationException, and then directly returned to the BaseResponse I defined.

terms of settlement

The solution is very simple. Directly delete the capture of InsufficientAuthenticationException exception.

Principle analysis

When we configure oauth/authorize as permitAll, direct access will be allowed when accessing the oauth/authorize interface. The oauth/authorize interface will judge the user's authorization information. If the authorization information fails, an exception will be thrown, and the code will enter the ExceptionTranslationFilter.

 

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
 
		try {
			chain.doFilter(request, response);
 
			logger.debug("Chain processed normally");
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
 
			if (ase == null) {
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			}
 
			if (ase != null) {
                 // Enter this method after catching the exception
				handleSpringSecurityException(request, response, chain, ase);
			}
			...
		}
	}
 
 
 
private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			logger.debug(
					"Authentication exception occurred; redirecting to authentication entry point",
					exception);
          
			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		}
        ...
}
 
 
protected void sendStartAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
        
		SecurityContextHolder.getContext().setAuthentication(null);
                // Save saveRequest in session to facilitate authentication after successful authentication.
		requestCache.saveRequest(request, response);
		logger.debug("Calling Authentication entry point.");
                // The request is redirected to the login interface, which is the configured loginPage of security
		authenticationEntryPoint.commence(request, response, reason);
}
 

The common implementation class of requestCache is HttpSessionRequestCache. Generally, the system judges that the user is not authorized when accessing the url, and the ExceptionTranslationFilter will save savedRequest to the session, named "SPRING_SECURITY_SAVED_REQUEST".

Savedrequest contains the previously accessed url address, cookie, header, parameter and other information. Once Authentication is successful, successhandler Onauthenticationsuccess (savedrequestawareauthenticationsuccesshandler) will extract savedrequest from the session and continue to access the original url.

public class HttpSessionRequestCache implements RequestCache {
	static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST";  
  /**
	 * HttpSessionRequestCache Stores the current request, provided the configuration properties allow it.
	 */
	public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
		if (requestMatcher.matches(request)) {
			DefaultSavedRequest savedRequest = new DefaultSavedRequest(request,
					portResolver);
 
			if (createSessionAllowed || request.getSession(false) != null) {
				// Store the HTTP request itself. Used by
				// AbstractAuthenticationProcessingFilter
				// for redirection after successful authentication (SEC-29)
				request.getSession().setAttribute(this.sessionAttrName, savedRequest);
				logger.debug("DefaultSavedRequest added to Session: " + savedRequest);
			}
		}
		else {
			logger.debug("Request not saved as configured RequestMatcher did not match");
		}
	}
}
public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {

		String redirectUrl = null;

		if (useForward) {

			if (forceHttps && "http".equals(request.getScheme())) {
				// First redirect the current request to HTTPS.
				// When that request is received, the forward to the login page will be
				// used.
				redirectUrl = buildHttpsRedirectUrlForRequest(request);
			}

			if (redirectUrl == null) {
				String loginForm = determineUrlToUseForThisRequest(request, response,
						authException);

				if (logger.isDebugEnabled()) {
					logger.debug("Server side forward to: " + loginForm);
				}

				RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

				dispatcher.forward(request, response);

				return;
			}
		}
		else {
			// redirect to login page. Use https if forceHttps true

			redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

		}

		redirectStrategy.sendRedirect(request, response, redirectUrl);
	}

Redirect to login

After successful login, jump to Baidu page and get the code authorization code

 

Use this authorization code to obtain access_token

Write at the end

In fact, it doesn't take long to solve the problem, but it takes a little time to understand the context. This article is also a small summary of myself. I think what I can write completely is what I really master. If there is anything wrong, please discuss it together.

Source code address involved in this article: https://github.com/airhonor/authority-management-sys-2.0

 

Topics: Java Spring Boot oauth2