Skip to content

support custom consent handler #740

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
chenzhenjia opened this issue May 16, 2022 · 19 comments
Closed

support custom consent handler #740

chenzhenjia opened this issue May 16, 2022 · 19 comments
Assignees
Labels
status: declined A suggestion or change that we don't feel we should currently apply

Comments

@chenzhenjia
Copy link

Hello, can you support custom sendAuthorizationConsent? response json or redirect

@chenzhenjia chenzhenjia added the type: enhancement A general enhancement label May 16, 2022
@sjohnr
Copy link
Member

sjohnr commented May 19, 2022

Hi @chenzhenjia, welcome to the project! I notice that you have opened a PR already. Please keep in mind it's always best to reach out and have a conversation prior to working on an issue, as there may be alternatives, feedback, and other conversation that needs to happen first.

Can you please describe more about your use case? Also, have you seen the custom consent sample in this repository? Can you explain what you're trying to accomplish that the sample does not demonstrate?

@sjohnr sjohnr self-assigned this May 19, 2022
@sjohnr sjohnr added the status: waiting-for-feedback We need additional information before we can continue label May 19, 2022
@chenzhenjia
Copy link
Author

chenzhenjia commented May 20, 2022

你好@chenzhenjia,欢迎来到项目!我注意到你已经打开了一个 PR。请记住,在解决问题之前最好先联系并进行对话,因为可能需要先进行替代方案、反馈和其他对话。

您能否详细描述一下您的用例?另外,您是否在此存储库中看到了自定义同意示例?你能解释一下你试图完成的样本没有展示什么吗?

My front end is developed with react and can only use json request, so I need to return json content
for example:
/oauth2/authorize?client_id=client&response_type=code redirect to login page
javascript:

const data = new URLSearchParams();
data.append("password", "xxx");
data.append("username", "xxx");
fetch("/login/password", {
    method: 'post',
    body: data,
})
.then((data)=>{
   let json =  data.json();
   // This page is the react page not the page returned by the servlet
    history.push(`/consent?client_id=${json.client_id}&state=${json.state}&scope=${json.scope}`);
});

my constent handler

if (response.isCommitted()) {  
  return;
}
String clientId = authorizationCodeRequestAuthenticationResult.getClientId();
String state = authorizationCodeRequestAuthenticationResult.getState();
Map<String, Object> result = new HashMap<>();
result.put("consent_required", authorizationCodeRequestAuthenticationResult.isConsentRequired());
result.put("consent", authorizationCodeRequestAuthenticationResult.isConsent());
result.put(OAuth2ParameterNames.CLIENT_ID, clientId);
result.put(OAuth2ParameterNames.STATE, state);
result.put(OAuth2ParameterNames.SCOPE, authorizationCodeRequestAuthentication.getScopes());
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(result));
response.getWriter().flush();

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 20, 2022
@sjohnr
Copy link
Member

sjohnr commented May 20, 2022

@chenzhenjia thanks for clarifying your use case. I'm curious why you are attempting to render a consent screen using react?

The reason I ask is that the authorization_code grant flow is redirect-based. I think it would be somewhat difficult and quite uncommon to build a javascript client to perform the entire flow, as XHR requests (with application/json) aren't designed to handle 302 responses (redirects) like the user agent (browser) is. So I'm puzzled why you'd want to change the consent screen to an isolated javascript application, given the rest of the flow is server-side rendered.

@chenzhenjia
Copy link
Author

@sjohnr Because our current project is already developed using react, and loginProcessingUrl
Returns json data, oauth2 is a newly added function, it needs to adapt to the api interface that has been developed before, I am a back-end developer in the company, the front-end framework is decided by our front-end team, I cannot decide that the front-end team uses Java services end rendering

My CustomOAuth2AuthenticationEntryPoint

public class CustomOAuth2AuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {

  public CustomAuthenticationEntryPoint(String loginPageUrl) {  
    super(loginPageUrl);
  }

  @Override
  protected String buildRedirectUrlToLoginPage(HttpServletRequest request,HttpServletResponse response, AuthenticationException authException) {  
    String httpUrl = super.buildRedirectUrlToLoginPage(request, response, authException); 
    UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpUrl);
    // request.getQueryString() Will bring client_id, scope, redirect_uri and other parameters
    return uriComponentsBuilder.query(Optional.ofNullable(request.getQueryString()).orElse("")).toUriString();
  }
}

My CustomAuthenticationFailureHandler

public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

  public final Map<Class<? extends AuthenticationException>, String> exceptionTypeMapping;

  public CustomAuthenticationFailureHandler() {
    this.exceptionTypeMapping = new HashMap<>();
    this.exceptionTypeMapping.put(BadCredentialsException.class, "bad_credentials");
    this.exceptionTypeMapping.put(AccountExpiredException.class, "account_expired");
    this.exceptionTypeMapping.put(DisabledException.class, "account_disabled");
    this.exceptionTypeMapping.put(UsernameNotFoundException.class, "username_not_found");
  }

  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException exception) throws IOException {
    HttpStatus status = HttpStatus.UNAUTHORIZED;
    response.setStatus(status.value());
    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");
    String type = Optional.ofNullable(exceptionTypeMapping.get(exception.getClass()))
        .orElse("authentication_failed");

    response.getWriter().write(
        "{\"" + OAuth2ParameterNames.ERROR + ":\"" + type + "\"," +
            OAuth2ParameterNames.ERROR_DESCRIPTION + "\":\""
            + exception.getMessage() + "\"}");
    response.flushBuffer();
  }
}

My CustomAuthenticationSuccessHandler

public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

  
  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException {
    clearAuthenticationAttributes(request);
    if (response.isCommitted()) {
      return;
    }
    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");
    response.setStatus(HttpServletResponse.SC_NO_CONTENT);
  }

  protected final void clearAuthenticationAttributes(HttpServletRequest request) {
    HttpSession session = request.getSession(false);
    if (session != null) {
      session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }
  }
}

@sjohnr
Copy link
Member

sjohnr commented May 24, 2022

Hi again @chenzhenjia, your description helps clarify your situation for me, thanks!

Regardless of whether you can or cannot render the frontend via the server, it's worth noting that the authorization server is responsible for deciding when consent is required, so the javascript application cannot assume that consent is always required. Looking over your provided sample javascript code, it looks as though the frontend assumes that the result of logging in is always a consent screen, which doesn't seem quite right. Combined with your sample AuthenticationSuccessHandler that returns 204 No Content, I am not seeing how the /oauth2/authorize endpoint is ever successfully invoked, as it would normally be a 302 Found redirect after authentication success.

Regarding this request, consent rendering should be customizable but I'm still stuck on whether it makes sense to customize using a "handler", as your PR does. I feel like it's redundant with the existing customization option of a consentPage which is exposed in OAuth2AuthorizationEndpointConfigurer.consentPage(). If you provide a custom consent page (e.g. consentPage("/consent"), it will be given the query string with client_id, state, and scope and it can easily return application/json. The OAuth2AuthorizationEndpointFilter will decide if consent is required and redirect to it after /oauth2/authorize is successful and return json in the response to the javascript application.

However there are a number of other configurations required to make this work, and as I said it seems challenging (though hopefully possible). Do you need help working through these steps, or am I off base somehow still? I'd appreciate it if you could try this suggestion and see if it makes the custom consent handler unnecessary, which I believe to be the case.

@sjohnr sjohnr added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels May 24, 2022
@chenzhenjia
Copy link
Author

@sjohnr The example I gave is the previous json processing, and the current one with oauth2 is like this

oauth2 OAuth2AuthenticationEntryPoint:

public class OAuth2AuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {

    private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
        new Base64StringKeyGenerator(Base64.getUrlEncoder());
  
    public OAuth2UrlAuthenticationEntryPoint(String loginFormUrl) {
      super(loginFormUrl);
    }
  
    @Override
    protected String buildRedirectUrlToLoginPage(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException authException) {
      String httpUrl = super.buildRedirectUrlToLoginPage(request, response, authException);
      UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpUrl)
          .query(Optional.ofNullable(request.getQueryString()).orElse(""));
      HttpSession session = request.getSession(false);
      if (session != null) {
        // add login_state query parma
        String loginState = DEFAULT_STATE_GENERATOR.generateKey();
        uriComponentsBuilder.queryParam("login_state", loginState);
        session.setAttribute(OAuth2Attributes.LOGIN_STATE, loginState);
      }
      return uriComponentsBuilder
          .toUriString();
    }
  }

update login AuthenticationSuccessHandler:

public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private PortResolver portResolver = new PortResolverImpl();
  
    public void setPortResolver(PortResolver portResolver) {
      this.portResolver = portResolver;
    }
  
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
        Authentication authentication) throws IOException {
      clearAuthenticationAttributes(request);
      if (response.isCommitted()) {
        return;
      }
      response.setContentType("application/json");
      response.setCharacterEncoding("UTF-8");
      HttpSession session = request.getSession(false);
      if (session != null) {
        Boolean oauth2Login = Optional.ofNullable(session.getAttribute(OAuth2Attributes.LOGIN_STATE))
            .filter(String.class::isInstance)
            .map(Object::toString)
            .map(s -> {
              String loginState = request.getParameter("loginState");
              return s.equals(loginState);
            })
            .orElse(false);
        // If it is oauth2, send authorize_url
        if (oauth2Login) {
          response.setStatus(HttpServletResponse.SC_OK);
          int serverPort = portResolver.getServerPort(request);
          String scheme = request.getScheme();
          RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
          urlBuilder.setScheme(scheme);
          urlBuilder.setServerName(request.getServerName());
          urlBuilder.setPort(serverPort);
          urlBuilder.setContextPath(request.getContextPath());
          urlBuilder.setPathInfo("/oauth2/authorize");
        
          response.getWriter().write("{\"auth_type\":\"oauth2\",\"authorize_url\":\"" + urlBuilder.getUrl() + "\"}");
          response.flushBuffer();
          return;
        }
      }
      response.setStatus(HttpServletResponse.SC_NO_CONTENT);
    }
  
    protected final void clearAuthenticationAttributes(HttpServletRequest request) {
      HttpSession session = request.getSession(false);
      if (session != null) {
        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
      }
    }
  }  

add OAuth2AuthenticationSuccessHandler

public class OAuth2uthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
        Authentication authentication) throws IOException, ServletException {
      OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
          (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
      UriComponentsBuilder uriBuilder = UriComponentsBuilder
          .fromUriString(authorizationCodeRequestAuthentication.getRedirectUri())
          .queryParam(OAuth2ParameterNames.CODE,
              authorizationCodeRequestAuthentication.getAuthorizationCode().getTokenValue());
      if (StringUtils.hasText(authorizationCodeRequestAuthentication.getState())) {
        uriBuilder.queryParam(OAuth2ParameterNames.STATE, authorizationCodeRequestAuthentication.getState());
      }
      response.setStatus(HttpServletResponse.SC_OK);
      response.setContentType("application/json;charset=UTF-8");
      response.setCharacterEncoding("UTF-8");
      response.getWriter().write("{\"" + OAuth2ParameterNames.REDIRECT_URI + "\":\"" + uriBuilder.toUriString() + "\"}");
    }
}

oauth2 configuration

  @Bean
  @Order(Ordered.HIGHEST_PRECEDENCE)
  public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
        new OAuth2AuthorizationServerConfigurer<>();
    authorizationServerConfigurer
        .authorizationEndpoint(authorizationEndpoint -> {
          authorizationEndpoint.consentHandler(new JsonOAuth2ConsentHandler())
              .authorizationResponseHandler(new OAuth2AuthenticationSuccessHandler())
              .errorResponseHandler(new OAuth2AuthenticationFailureHandler());
        })
    ;

    RequestMatcher oauth2EndpointsMatcher = authorizationServerConfigurer
        .getEndpointsMatcher();

    return http
        .requestMatchers(requestMatchers ->
            requestMatchers.requestMatchers(oauth2EndpointsMatcher)
        )
        .authorizeRequests(authorizeRequests -> authorizeRequests
            .anyRequest().authenticated()
        )
        .sessionManagement(
            sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
        )
        .csrf(AbstractHttpConfigurer::disable)
        .formLogin()
        .and()
        .httpBasic()
        .disable()
        .exceptionHandling(exceptionHandling ->
            exceptionHandling
                .defaultAuthenticationEntryPointFor(
                    new OAuth2AuthenticationEntryPoint("/login"),
                    AnyRequestMatcher.INSTANCE)
        )
        .apply(authorizationServerConfigurer)
        .and()
        .build();
  }
const data = new URLSearchParams();
data.append("password", "xxx");
data.append("username", "xxx");
fetch("/login/password", {
    method: 'post',
    body: data,
})
.then((data)=>{
   let json =  data.json();
   if(json.auth_type==='oauth2'){
      return  fetch(json.authorize_url, {
            method: 'post'
        }).then(data1=>data1.json());
        
    }
    return json;
})
.then(json=>{
    // consent handler
    if(json.consent_required){
        // this consent page is react router page not servlet response page 
        history.push(`/consent?client_id=${json.client_id}&state=${json.state}&scope=${json.scope}`);
    }
    // oauth2 success handler
    if(json.redirect_uri){
        // Consent is not required
        window.location.href=json.redirect_uri
    }
    // default login success handler 
    // xxxx
})
;

use google translate,please understand

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 25, 2022
@sjohnr
Copy link
Member

sjohnr commented May 25, 2022

Hi @chenzhenjia.

use google translate,please understand

I apologize, I did not realize that. I will try and be clear and concise!

Instead of a "consent handler" you can give this a try:

  Bean
  @Order(Ordered.HIGHEST_PRECEDENCE)
  public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
        new OAuth2AuthorizationServerConfigurer<>();
    authorizationServerConfigurer
        .authorizationEndpoint(authorizationEndpoint -> {
          authorizationEndpoint.consentPage("/consent")
              .authorizationResponseHandler(new OAuth2AuthenticationSuccessHandler())
              .errorResponseHandler(new OAuth2AuthenticationFailureHandler());
        });

    ...
  }
@RestController
public class ConsentController {
  @GetMapping("/consent")
  public Map<String, String> jsonConsent(
      @RequestParam("client_id") String clientId,
      @RequestParam("state") String state,
      @RequestParam("scope") String scope) {
    ...
  }
}

@chenzhenjia
Copy link
Author

Hi @sjohnr Now consent is redirection this.redirectStrategy.sendRedirect(request, response, redirectUri);

Request /oauth2/authorize redirect to /consent client is inconvenient to handle, now need to return json data instead of redirect
consent response json

Map<String, Object> result = new HashMap<>();
result.put("consent_required", authorizationCodeRequestAuthenticationResult.isConsentRequired());
result.put("consent", authorizationCodeRequestAuthenticationResult.isConsent());
result.put(OAuth2ParameterNames.CLIENT_ID, clientId);
result.put(OAuth2ParameterNames.STATE, state);
result.put(OAuth2ParameterNames.SCOPE, authorizationCodeRequestAuthentication.getScopes());
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(result));

javascript login success consent required handler

if(json.consent_required){
  // this consent page is react router page not servlet response page 
  history.push(`/consent?client_id=${json.client_id}&state=${json.state}&scope=${json.scope}`);
}

java /oauth2/consent rest controller

  @GetMapping(value = "/oauth2/consent", consumes = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public ResponseEntity<?> consent(Principal principal,
      @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
      @RequestParam(OAuth2ParameterNames.SCOPE) String scope,
      @RequestParam(OAuth2ParameterNames.STATE) String state) {
    // xxxx     
    return ResponseEntity.ok(model);
  }

javascript consent router page,This page can only be jumped through react routing

let client_id = // from query params
let state = // from query params
let scope = // from query params
fetch(`/oauth2/consent?client_id=${client_id}&state=${state}&scope=${scope}`)
.then(data=>{
  // react set state and render view
})
// User submits form
let handlerSubmit=()=>{
const data = new URLSearchParams();
data.append("client_id", "client_id");
data.append("state", "state");
data.append("scope", scopes);
fetch(`/oauth2/authorize`,{
  method:"post",
  data:data
})
.then(data=>{
  // oauth2 success handler
  if(json.redirect_uri){
      // Consent is not required
      window.location.href=json.redirect_uri
  }
})

}

@sjohnr
Copy link
Member

sjohnr commented May 26, 2022

@chenzhenjia,

Request /oauth2/authorize redirect to /consent client is inconvenient to handle

I would like to confirm whether it is possible to handle a 302 redirect to a json consent endpoint, even if it is inconvenient.

@jgrandja
Copy link
Collaborator

jgrandja commented May 26, 2022

@chenzhenjia Regarding your comment:

Because our current project is already developed using react, and loginProcessingUrl
Returns json data, oauth2 is a newly added function, it needs to adapt to the api interface that has been developed before, I am a back-end developer in the company, the front-end framework is decided by our front-end team, I cannot decide that the front-end team uses Java services end rendering

  1. As far as how your teams are structured and the technology stacks they use, that is something you will need to sort out. We cannot simply add a new API to make things convenient. Our ultimate goal is to address the major use cases so the developer experience is seamless for the majority. However, we cannot address all use cases and as far as I recall this is the first time I'm seeing such a use case so it more than likely will not be enhanced unless I'm missing something.

  2. The authentication flow (loginProcessingUrl) in your current project cannot be compared to the authorization_code grant flow since there are 2 steps to that flow (authorize and token calls) and potentially a 3rd step with consent. It is much more complicated than the authentication flow, so I disagree with this point

it needs to adapt to the api interface that has been developed before

Regarding...

use google translate,please understand

It is extremely difficult to understand such a complicated (and unique) flow with cut-copy-paste code. Please put together a minimal sample and document the gaps in the sample. This way we can see exactly what extension points are missing (if any).

I'm not sure if you saw gh-297? We've collaborated with front-end developers that are using Spring Authorization Server successfully so reading over that issue may help. @sjohnr Can you also please forward the samples for reference.

As @sjohnr mentioned:

Please keep in mind it's always best to reach out and have a conversation prior to working on an issue, as there may be alternatives, feedback, and other conversation that needs to happen first.

I'm going to close the PR as we're not looking to add a new API at this point.

The next step is to review the minimal sample you need to provide so we can better understand your application requirements and it will allow us to determine if a new API is needed or if there is an alternative solution with an existing API.

Please ensure the sample has the absolute minimal amount of code necessary to demonstrate the flows and it's well documented so we can be efficient in our analysis.

@jgrandja jgrandja added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels May 26, 2022
@sjohnr
Copy link
Member

sjohnr commented May 26, 2022

@chenzhenjia here are some sample projects on a branch for you to review:

For context, please see the Single Page Apps webinar Joe and I presented. The audio is in English but you can follow the video to see the code samples in action.

@tomasjaros
Copy link

Hello everyone,
I would also vote for adding support for some kind of consent handling customization. In my case, I process and store consents outside of my Authorization server and I need to redirect to the consent URI which must be built individually for each request - URIs will differ for various client_ids and I also want to add an extra query parameter to the redirect URI.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 29, 2022
@chenzhenjia
Copy link
Author

@jgrandja Here is my sample code chenzhenjia@745e6e7

@sjohnr The example you gave is to authorize the client, not the server

@NotFound403
Copy link
Contributor

NotFound403 commented Jun 8, 2022

@chenzhenjia Consent page must be controlled securely by Auth Server,location.href is unsafe that easy to be tampered,so this is not a good practise.

@chenzhenjia
Copy link
Author

@NotFound403 In which step will it be tampered with

@NotFound403
Copy link
Contributor

NotFound403 commented Jun 8, 2022

@NotFound403 In which step will it be tampered with

@chenzhenjia its an Open Redirect ,not be validated.

@chenzhenjia
Copy link
Author

@NotFound403 consent is the react routing page, instead of using localtion.href, it uses react router. Localtion.href will only be used if the authorization is successful. redirect_uri is returned to the client after verification by the server, so it can be trusted.

@jgrandja
Copy link
Collaborator

@chenzhenjia Thanks for the sample. This was very helpful in determining your implementation strategy.

I'll provide a few points on my findings based on your authorization server configuration:

OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
		new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer
		.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint
				.consentHandler(new JsonOAuth2ConsentHandler())
				.authorizationResponseHandler(new
						OAuth2JsonAuthenticationSuccessHandler())
				.errorResponseHandler(new OAuth2JsonAuthenticationFailureHandler())
		);

From my understanding, you're mainly looking to address the consent flow via consentHandler() (JsonOAuth2ConsentHandler), however, I would like to start off with authorizationResponseHandler() and errorResponseHandler().

The authorizationResponseHandler() (OAuth2JsonAuthenticationSuccessHandler) and errorResponseHandler() (OAuth2JsonAuthenticationFailureHandler) implementations do not conform to the protocol flow as defined in the spec.

Here are some main references to refer to:

Section 1.3.1:

An authorization code is a temporary credential used to obtain an
access token. Instead of the client requesting authorization
directly from the resource owner, the client directs the resource
owner to an authorization server (via its user agent, which in turn
directs the resource owner back to the client with the authorization
code.
The client can then exchange the authorization code for an
access token.

Section 4.1:

Since this is a redirect-based flow, the client must be capable of
initiating the flow with the resource owner's user agent (typically a
web browser) and capable of being redirected back to from the
authorization server.

Section 4.1.2.1:

If the resource owner denies the access request or if the request
fails for reasons other than a missing or invalid redirect URI, the
authorization server informs the client by adding the following
parameters to the query component of the redirect URI using the
application/x-www-form-urlencoded format

Even if you're able to get it working without redirects, you may encounter unexpected behaviour or potentially expose yourself to a vulnerability. The main point I want to relay here is that the protocol flow deviates away from the spec and is not something we would support. Our main goal is to implement to spec and provide extension points that applications can customize against.

Moving to the consentHandler() (JsonOAuth2ConsentHandler), @sjohnr has already provided you a solution in this comment. From my understanding, the reason you logged this issue (and associated PR) is you would like to return a json response for the authorization consent page. I tried @sjohnr suggestion and it meets your requirements. I used the custom-consent-authorizationserver sample and updated the AuthorizationConsentController as follows:

@RestController
public class AuthorizationConsentController {
	private final RegisteredClientRepository registeredClientRepository;
	private final OAuth2AuthorizationConsentService authorizationConsentService;

	public AuthorizationConsentController(RegisteredClientRepository registeredClientRepository,
			OAuth2AuthorizationConsentService authorizationConsentService) {
		this.registeredClientRepository = registeredClientRepository;
		this.authorizationConsentService = authorizationConsentService;
	}

	@GetMapping(value = "/oauth2/consent")
	public Map<String, Object> consent(Principal principal,
			@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
			@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
			@RequestParam(OAuth2ParameterNames.STATE) String state) {

		// Remove scopes that were already approved
		Set<String> scopesToApprove = new HashSet<>();
		Set<String> previouslyApprovedScopes = new HashSet<>();
		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
		OAuth2AuthorizationConsent currentAuthorizationConsent =
				this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());
		Set<String> authorizedScopes;
		if (currentAuthorizationConsent != null) {
			authorizedScopes = currentAuthorizationConsent.getScopes();
		} else {
			authorizedScopes = Collections.emptySet();
		}
		for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
			if (authorizedScopes.contains(requestedScope)) {
				previouslyApprovedScopes.add(requestedScope);
			} else {
				scopesToApprove.add(requestedScope);
			}
		}

		Map<String, Object> model = new HashMap<>();
		model.put("clientId", clientId);
		model.put("state", state);
		model.put("scopes", withDescription(scopesToApprove));
		model.put("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));
		model.put("principalName", principal.getName());

		return model;
	}

	...

}

The json response returned is:

{
"clientId": "messaging-client",
"previouslyApprovedScopes": [],
"principalName": "user1",
"state": "Ug37PXnTJg1J_JrI_SeHcpPMatpfwJ1kQErEr-ln0YU=",
"scopes": [
{
"scope": "message.write",
"description": "This application will be able to add new messages. It will also be able to edit and delete existing messages."
},
{
"scope": "message.read",
"description": "This application will be able to read your message."
}
]
}

As demonstrated with the above code sample, the configuration option authorizationEndpoint.consentPage() can meet your specialized requirements and there is no current need to introduce a new API (OAuth2ConsentHandler) for this.

@jgrandja jgrandja added status: declined A suggestion or change that we don't feel we should currently apply and removed status: feedback-provided Feedback has been provided type: enhancement A general enhancement labels Jun 13, 2022
@jgrandja jgrandja assigned jgrandja and unassigned sjohnr Jun 13, 2022
@wilsonlv
Copy link

if the authorization-server is behind of a gateway, the 302 redirect can not go to a correct uri.
for example, the gateway server address is 127.0.0.1 and the authorization-server is 127.0.0.1:8888, then the 302 redirect url is 127.0.0.1:8888/oauth2/consent, this will result in a cross-domain request, so how can we resolve it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants