NGINX Client Control by Spring Cloud Gateway

Deniz G
4 min readAug 18, 2021

If you have integrated the security of Spring Cloud Gateway with OAuth2 Authorization Code grant type, let me start by reminding this flow.

if a unauthenticated user sends a request to the Spring Cloud Gateway, user will get a response with a 302 return code indicating that user must log in at the authorization server.

We can perform user control by Spring Cloud Gateway in static files / frontend codes that we will return to the user via NGINX.

NGINX User Control

  1. Authentication: Frontend codes / static files should not be shown to users who are not unauthenticated.
  2. Authorization: The authenticated user should not be able to access the frontend / static files to which user is not authorized.

1. Authentication Control

In normal case, the directive serving the client is as follows.

location /my-react-webclient {
rewrite ^ /my-react-webclient/index.html break;
}

In this case, client codes are returned regardless of whether the incoming request is from an authenticated source.

location /my-react-webclient {
auth_request /_<user-control>;
error_page <code>= @<error-handler>;
rewrite ^ /my-react-webclient/index.html break;
}

Before serving client codes, a user check can be made with the auth_request directive. If it is understood that there is a unauthenticated user during this check, the login flow is started with the error_page directive for the user to log in. If there is no error during the check, which means that the user is logged in, the client is served.

Let’s do a sample check using this information.

location /my-react-webclient {
auth_request /_user_control;
error_page 401 = @handleResult;

rewrite ^ /my-react-webclient/index.html break;
}

location /_user_control {
internal;
proxy_method POST;
proxy_pass http://gateway-url/webclient/check;
proxy_intercept_errors on;
recursive_error_pages on;

error_page 301 302 = @handleNotAuthenticated;
}

location @handleNotAuthenticated {
return 401;
}

location @handleResult {
internal;
return 302 http://gateway-url/webclient/check;
}
  1. All incoming requests go to /_user_control first.
  2. /_user_control makes a request to /webclient/check defined in Spring Cloud Gateway. If the response is 302 (Spring Cloud Gateway returns a 302 response for requests from users who are unauthorized. 302 status code gives the address of the Authorization Server that users should log in to), then enters @handleNotAuthenticated.
  3. The auth_request directive only accepts 200, 401 and 403 status codes. All other errors are evaluated as 500. so, in @handleNotAuthenticated, 302 is converted to 401 and /_user_control subrequest returns 401 response, completing the internal call.
  4. NGINX accepts 401 returns from subrequest as errors (because we defined error_page 401) and puts it in @handleResult.
  5. @handleResult returns a 302 to the client indicating that the user should request /webclient/check. After the user receives this response, the browser automatically requests /webclient/check and from there, the user’s login process is started (the flow that i mentioned at first).
  6. If /_user_control returned 200, there is no need for steps 2, 3–4–5. Frontend codes / static files would be returned to the user directly.

PS: Spring Cloud Gateway Route used during the process

- id: webclient_check_route
predicates:
- Path=/webclient/check
filters:
- SetStatus=200
uri: no://op

2. Authorization Control

While checking whether the user is authenticated or not, it is also possible to check whether user is authorized or not.

location /my-react-webclient {
auth_request /_user_control;
error_page 401 = @handleResult;

rewrite ^ /my-react-webclient/index.html break;
}

location /_user_control {
internal;
proxy_method POST;
proxy_pass http://gateway-url/webclient/check;
proxy_intercept_errors on;
recursive_error_pages on;

error_page 301 302 = @handleNotAuthenticated;
error_page 406 = @handleAuthorization;
}

location @handleNotAuthenticated {
return 401;
}

location @handleAuthorization {
internal;
set $redirect_url http://gateway-url/scope/check;
proxy_method POST;
proxy_pass $redirect_url;
proxy_intercept_errors on;
recursive_error_pages on;
proxy_set_body $cookie_jwt;
}

location @handleResult {
internal;
return 302 http://gateway-url/webclient/check;
}

1–2–3–4–5 steps are the same as the authentication steps defined on the previous. The flow added for authorization goes as follows.

6. If it returned 406 from /_user_control in step 2, it indicates that the user is authenticated but requires authorization. In this case (error_page 406) goes into @handleAuthorization.

7. @handleAuthorization makes a request to the /scope/check endpoint defined on the Spring Cloud Gateway. This endpoint accepts JWT in the request body and it returns 200 if authorized, 403 if unauthorized. You must define this endpoint and its contents.

@PostMapping("/scope/check")
public void checkAppname(@RequestBody String apikey, ServerHttpResponse response) {
log.info("checking access token {}", apikey);
// check authorization process
if(...) {
response.setStatusCode(HttpStatus.OK);
} else {
response.setStatusCode(HttpStatus.FORBIDDEN);
}
}

8. The results returned from here don’t need to be handled, because they are response codes accepted by NGINX. If 200 returns, frontend codes / static files are served to the user. If 403 returns, the NGINX’s default forbidden screen is served.

PS: Spring Cloud Gateway Route used during the process

- id: webclient_check_route
predicates:
- Path=/webclient/check
filters:
- SetStatus=406
uri: no://op

--

--