Skip to content

Commit

Permalink
Add back authorizationUrl redirect feature with a flag (#828)
Browse files Browse the repository at this point in the history
* Revert "remove authorizationUrl redirect (#824)"

This reverts commit 621211d.

* Add redirect_authorization_url flag

Signed-off-by: Wayne Zhang <[email protected]>

* rename the flag

Signed-off-by: Wayne Zhang <[email protected]>
  • Loading branch information
qiwzhang authored Jan 15, 2021
1 parent 3131cc0 commit f67ffdc
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 2 deletions.
4 changes: 3 additions & 1 deletion src/api_manager/context/global_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ GlobalContext::GlobalContext(std::unique_ptr<ApiManagerEnvInterface> env,
preserve_proto_field_names_(false),
intermediate_report_interval_(kIntermediateReportInterval),
platform_(ComputePlatform::kUnknown),
jwks_cache_duration_in_s_(kPubKeyCacheDurationInSecond) {
jwks_cache_duration_in_s_(kPubKeyCacheDurationInSecond),
redirect_authorization_url_(false) {
// Need to load server config first.
server_config_ = Config::LoadServerConfig(env_.get(), server_config);

Expand All @@ -79,6 +80,7 @@ GlobalContext::GlobalContext(std::unique_ptr<ApiManagerEnvInterface> env,
if (auth_config.jwks_cache_duration_in_s() > 0) {
jwks_cache_duration_in_s_ = auth_config.jwks_cache_duration_in_s();
}
redirect_authorization_url_ = auth_config.redirect_authorization_url();
}

// Check server_config override.
Expand Down
6 changes: 6 additions & 0 deletions src/api_manager/context/global_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ class GlobalContext {
const std::string &location() const { return location_; }

int jwks_cache_duration_in_s() const { return jwks_cache_duration_in_s_; }
bool redirect_authorization_url() const {
return redirect_authorization_url_;
}

void set_rollout_id_func(SetRolloutIdFunc rollout_id_func) {
rollout_id_func_ = rollout_id_func;
Expand Down Expand Up @@ -152,6 +155,9 @@ class GlobalContext {
// The jwks public key cache duration.
int jwks_cache_duration_in_s_;

// enable to redirect to authorizationUrl
bool redirect_authorization_url_;

// The function to set rollout id fetched from Check and Report response.
SetRolloutIdFunc rollout_id_func_;
};
Expand Down
4 changes: 4 additions & 0 deletions src/api_manager/context/request_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,10 @@ std::string RequestContext::GetAuthorizationUrl() const {
if (method_call_.method_info == nullptr) {
return "";
}
// This feature has to be enabled from the flag
if (!service_context()->global_context()->redirect_authorization_url()) {
return "";
}
if (auth_issuer_.empty()) {
return method_call_.method_info->first_authorization_url();
} else {
Expand Down
4 changes: 4 additions & 0 deletions src/api_manager/proto/server_config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ message ApiAuthenticationConfig {
// Specify the JWKS public key cache duration in seconds.
// If not specified, or 0, default is 300 (5 minutes).
int32 jwks_cache_duration_in_s = 2;

// If true, authentication failed requests will be redirected to
// the URL specified by "authorizationUrl" field in OpenAPI spec.
bool redirect_authorization_url = 3;
}

// Server config for API Authorization via Firebase Rules
Expand Down
43 changes: 43 additions & 0 deletions src/nginx/error.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,16 @@ ngx_str_t application_grpc = ngx_string("application/grpc");

ngx_str_t www_authenticate = ngx_string("WWW-Authenticate");
const u_char www_authenticate_lowcase[] = "www-authenticate";
ngx_str_t kLocation = ngx_string("Location");
const u_char kLocationLowcase[] = "location";
ngx_str_t missing_credential = ngx_string("Bearer");
ngx_str_t invalid_token = ngx_string("Bearer, error=\"invalid_token\"");

const char *kInvalidAuthToken =
"JWT validation failed: Missing or invalid credentials";
const char *kExpiredAuthToken =
"JWT validation failed: TIME_CONSTRAINT_FAILURE";

ngx_http_output_header_filter_pt ngx_http_next_header_filter;
ngx_http_output_body_filter_pt ngx_http_next_body_filter;

Expand Down Expand Up @@ -102,6 +109,39 @@ ngx_int_t ngx_esp_handle_www_authenticate(ngx_http_request_t *r,
return NGX_OK;
}

// If authentication fails, and authorization url is not empty,
// Reply 302 and authorization url.
ngx_int_t ngx_esp_handle_authorization_url(ngx_http_request_t *r,
ngx_esp_request_ctx_t *ctx) {
if (ctx && ctx->status.code() == Code::UNAUTHENTICATED &&
ctx->status.error_cause() == utils::Status::AUTH &&
(ctx->status.message() == kInvalidAuthToken ||
ctx->status.message() == kExpiredAuthToken)) {
std::string url = ctx->request_handler->GetAuthorizationUrl();
if (!url.empty()) {
r->headers_out.status = NGX_HTTP_MOVED_TEMPORARILY;

ngx_table_elt_t *loc;
loc = reinterpret_cast<ngx_table_elt_t *>(
ngx_list_push(&r->headers_out.headers));
if (loc == nullptr) {
return NGX_ERROR;
}

loc->key = kLocation;
loc->lowcase_key = const_cast<u_char *>(kLocationLowcase);
loc->hash = ngx_hash_key(const_cast<u_char *>(kLocationLowcase),
sizeof(kLocationLowcase) - 1);

ngx_str_copy_from_std(r->pool, url, &loc->value);
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"ESP authorization_url: %V", &loc->value);
r->headers_out.location = loc;
}
}
return NGX_OK;
}

ngx_int_t ngx_esp_error_header_filter(ngx_http_request_t *r) {
ngx_esp_request_ctx_t *ctx = reinterpret_cast<ngx_esp_request_ctx_t *>(
ngx_http_get_module_ctx(r, ngx_esp_module));
Expand Down Expand Up @@ -130,6 +170,9 @@ ngx_int_t ngx_esp_error_header_filter(ngx_http_request_t *r) {
ngx_int_t ret;
ret = ngx_esp_handle_www_authenticate(r, ctx);
if (ret != NGX_OK) return ret;

ret = ngx_esp_handle_authorization_url(r, ctx);
if (ret != NGX_OK) return ret;
}

// Clear headers (refilled by subsequent NGX header filters)
Expand Down
1 change: 1 addition & 0 deletions src/nginx/t/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ nginx_suite(
"auth_ok_check_fail.t",
"auth_pass_user_info.t",
"auth_pkey_cache.t",
"auth_redirect.t",
"auth_remove_user_info.t",
"auth_unreachable_pkey.t",
"new_http.t",
Expand Down
1 change: 1 addition & 0 deletions src/nginx/t/auth_pkey.t
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ authentication {
id: "test_auth"
issuer: "628645741881-noabiu23f5a8m8ovd8ucv698lj78vv0l\@developer.gserviceaccount.com"
jwks_uri: "http://127.0.0.1:${PubkeyPort}/pubkey"
authorization_url: "http://dummy-redirect-url"
}
rules {
selector: "ListShelves"
Expand Down
138 changes: 138 additions & 0 deletions src/nginx/t/auth_redirect.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Copyright (C) Extensible Service Proxy Authors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
################################################################################
#
use strict;
use warnings;

################################################################################

use src::nginx::t::ApiManager; # Must be first (sets up import path to the Nginx test module)
use src::nginx::t::HttpServer;
use src::nginx::t::ServiceControl;
use src::nginx::t::Auth;
use Test::Nginx; # Imports Nginx's test module
use Test::More; # And the test framework

################################################################################

# Port allocations

my $NginxPort = ApiManager::pick_port();

my $t = Test::Nginx->new()->has(qw/http proxy/)->plan(5);

my $config = ApiManager::get_bookstore_service_config .<<"EOF";
producer_project_id: "endpoints-test"
authentication {
providers {
id: "test_auth"
issuer: "628645741881-noabiu23f5a8m8ovd8ucv698lj78vv0l\@developer.gserviceaccount.com"
jwks_uri: "http://127.0.0.1/pubkey"
authorization_url: "http://dummy-redirect-url"
}
rules {
selector: "ListShelves"
requirements {
provider_id: "test_auth"
audiences: "ok_audience_1"
}
}
}
control {
environment: "http://127.0.0.1:3000"
}
EOF
$t->write_file('service.pb.txt', $config);

# enable the redirect_authorization_url flag
ApiManager::write_file_expand($t, 'server_config.txt', <<"EOF");
api_authentication_config {
redirect_authorization_url: true
}
EOF

$t->write_file_expand('nginx.conf', <<"EOF");
%%TEST_GLOBALS%%
daemon off;
events {
worker_connections 32;
}
http {
%%TEST_GLOBALS_HTTP%%
server_tokens off;
server {
listen 127.0.0.1:${NginxPort};
server_name localhost;
location / {
endpoints {
api service.pb.txt;
server_config server_config.txt;
on;
}
proxy_pass http://127.0.0.1:3000;
}
}
}
EOF

$t->run();

################################################################################
# no auth token
my $response1 = ApiManager::http_get($NginxPort, "/shelves");

# should redirect
like($response1, qr/HTTP\/1\.1 302 Moved Temporarily/, 'Returned HTTP 302.');
like($response1, qr/Location: http:\/\/dummy-redirect-url/, 'Return correct redirect location.');

# expired auth token
my $expired_token = Auth::get_expired_jwt_token;
my $response2 = ApiManager::http($NginxPort, <<"EOF");
GET /shelves HTTP/1.0
Host: localhost
Authorization: Bearer $expired_token
EOF

# should redirect
like($response2, qr/HTTP\/1\.1 302 Moved Temporarily/, 'Returned HTTP 302.');
like($response2, qr/Location: http:\/\/dummy-redirect-url/, 'Return correct redirect location.');

#invalid auth token
my $response3 = ApiManager::http($NginxPort, <<"EOF");
GET /shelves HTTP/1.0
Host: localhost
Authorization: Bearer invalid_token
EOF

# should not redirect.
like($response3, qr/HTTP\/1\.1 401 Unauthorized/, 'Returned HTTP 401.');

$t->stop_daemons();

################################################################################

5 changes: 4 additions & 1 deletion start_esp/server-auto.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,14 @@ service_control_config {
% endif
}
% endif
% if jwks_cache_duration_in_s:
% if jwks_cache_duration_in_s or redirect_authorization_url:
api_authentication_config {
% if jwks_cache_duration_in_s:
jwks_cache_duration_in_s: ${jwks_cache_duration_in_s}
% endif
% if redirect_authorization_url:
redirect_authorization_url: true
% endif
}
% endif
% if client_ip_header:
Expand Down
6 changes: 6 additions & 0 deletions start_esp/start_esp.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ def write_server_config_template(server_config_path, args):
compute_platform_override=args.compute_platform_override,
grpc_backend_ssl_credentials=args.grpc_backend_ssl_credentials,
jwks_cache_duration_in_s=args.jwks_cache_duration_in_s,
redirect_authorization_url=args.enable_jwt_authorization_url_redirect,
rollout_fetch_throttle_window_in_s=args.rollout_fetch_throttle_window_in_s)

server_config_file = server_config_path
Expand Down Expand Up @@ -939,6 +940,11 @@ def make_argparser():
parser.add_argument('--jwks_cache_duration_in_s', default=None, type=int, help='''
Specify JWT public key cache duration in seconds. Default is 5 minutes.''')

parser.add_argument('--enable_jwt_authorization_url_redirect', action='store_true',
help='''If set to true, authentication failed requests will be redirected
to the URL specified by the `authorizationUrl` field in OpenAPI specification.
The default is false.''')

parser.add_argument('--rollout_fetch_throttle_window_in_s', default=None, type=int, help='''
When a new rollout is detected, ESP will call ServiceManagement to get the
new service configs. ServiceManagement API has a low calling quota, in order not
Expand Down
5 changes: 5 additions & 0 deletions start_esp/test/start_esp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,11 @@ def test_rollout_fetch_throttle_window_is_as_expected(self):
config_generator = self.basic_config_generator + " --rollout_fetch_throttle_window_in_s 1000"
self.run_test_with_expectation(expected_config_file, self.generated_server_config_file, config_generator)

def test_redirect_authorization_url(self):
expected_config_file = "./start_esp/test/testdata/expected_redirect_authorization_url.json"
config_generator = self.basic_config_generator + " --enable_jwt_authorization_url_redirect"
self.run_test_with_expectation(expected_config_file, self.generated_server_config_file, config_generator)

########## The tests for validating it should generate failure on conflict flags ##########

def test_enable_backend_routing_conflicts_with_string_flag(self):
Expand Down
43 changes: 43 additions & 0 deletions start_esp/test/testdata/expected_redirect_authorization_url.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Auto-generated by start_esp
# Copyright (C) Extensible Service Proxy Authors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
service_config_rollout {
traffic_percentages {
key: "./start_esp/test/testdata/test_service_config_1.json"
value: 100
}
}
service_management_config {
url: "https://servicemanagement.googleapis.com"
}
service_control_config {
network_fail_open: true
}
api_authentication_config {
redirect_authorization_url: true
}
experimental {
disable_log_status: true
}
rollout_strategy: "None"

0 comments on commit f67ffdc

Please sign in to comment.