Skip to content

Commit f67ffdc

Browse files
authored
Add back authorizationUrl redirect feature with a flag (#828)
* Revert "remove authorizationUrl redirect (#824)" This reverts commit 621211d. * Add redirect_authorization_url flag Signed-off-by: Wayne Zhang <qiwzhang@google.com> * rename the flag Signed-off-by: Wayne Zhang <qiwzhang@google.com>
1 parent 3131cc0 commit f67ffdc

12 files changed

Lines changed: 258 additions & 2 deletions

src/api_manager/context/global_context.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ GlobalContext::GlobalContext(std::unique_ptr<ApiManagerEnvInterface> env,
5656
preserve_proto_field_names_(false),
5757
intermediate_report_interval_(kIntermediateReportInterval),
5858
platform_(ComputePlatform::kUnknown),
59-
jwks_cache_duration_in_s_(kPubKeyCacheDurationInSecond) {
59+
jwks_cache_duration_in_s_(kPubKeyCacheDurationInSecond),
60+
redirect_authorization_url_(false) {
6061
// Need to load server config first.
6162
server_config_ = Config::LoadServerConfig(env_.get(), server_config);
6263

@@ -79,6 +80,7 @@ GlobalContext::GlobalContext(std::unique_ptr<ApiManagerEnvInterface> env,
7980
if (auth_config.jwks_cache_duration_in_s() > 0) {
8081
jwks_cache_duration_in_s_ = auth_config.jwks_cache_duration_in_s();
8182
}
83+
redirect_authorization_url_ = auth_config.redirect_authorization_url();
8284
}
8385

8486
// Check server_config override.

src/api_manager/context/global_context.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ class GlobalContext {
9898
const std::string &location() const { return location_; }
9999

100100
int jwks_cache_duration_in_s() const { return jwks_cache_duration_in_s_; }
101+
bool redirect_authorization_url() const {
102+
return redirect_authorization_url_;
103+
}
101104

102105
void set_rollout_id_func(SetRolloutIdFunc rollout_id_func) {
103106
rollout_id_func_ = rollout_id_func;
@@ -152,6 +155,9 @@ class GlobalContext {
152155
// The jwks public key cache duration.
153156
int jwks_cache_duration_in_s_;
154157

158+
// enable to redirect to authorizationUrl
159+
bool redirect_authorization_url_;
160+
155161
// The function to set rollout id fetched from Check and Report response.
156162
SetRolloutIdFunc rollout_id_func_;
157163
};

src/api_manager/context/request_context.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,10 @@ std::string RequestContext::GetAuthorizationUrl() const {
445445
if (method_call_.method_info == nullptr) {
446446
return "";
447447
}
448+
// This feature has to be enabled from the flag
449+
if (!service_context()->global_context()->redirect_authorization_url()) {
450+
return "";
451+
}
448452
if (auth_issuer_.empty()) {
449453
return method_call_.method_info->first_authorization_url();
450454
} else {

src/api_manager/proto/server_config.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ message ApiAuthenticationConfig {
214214
// Specify the JWKS public key cache duration in seconds.
215215
// If not specified, or 0, default is 300 (5 minutes).
216216
int32 jwks_cache_duration_in_s = 2;
217+
218+
// If true, authentication failed requests will be redirected to
219+
// the URL specified by "authorizationUrl" field in OpenAPI spec.
220+
bool redirect_authorization_url = 3;
217221
}
218222

219223
// Server config for API Authorization via Firebase Rules

src/nginx/error.cc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,16 @@ ngx_str_t application_grpc = ngx_string("application/grpc");
4747

4848
ngx_str_t www_authenticate = ngx_string("WWW-Authenticate");
4949
const u_char www_authenticate_lowcase[] = "www-authenticate";
50+
ngx_str_t kLocation = ngx_string("Location");
51+
const u_char kLocationLowcase[] = "location";
5052
ngx_str_t missing_credential = ngx_string("Bearer");
5153
ngx_str_t invalid_token = ngx_string("Bearer, error=\"invalid_token\"");
5254

55+
const char *kInvalidAuthToken =
56+
"JWT validation failed: Missing or invalid credentials";
57+
const char *kExpiredAuthToken =
58+
"JWT validation failed: TIME_CONSTRAINT_FAILURE";
59+
5360
ngx_http_output_header_filter_pt ngx_http_next_header_filter;
5461
ngx_http_output_body_filter_pt ngx_http_next_body_filter;
5562

@@ -102,6 +109,39 @@ ngx_int_t ngx_esp_handle_www_authenticate(ngx_http_request_t *r,
102109
return NGX_OK;
103110
}
104111

112+
// If authentication fails, and authorization url is not empty,
113+
// Reply 302 and authorization url.
114+
ngx_int_t ngx_esp_handle_authorization_url(ngx_http_request_t *r,
115+
ngx_esp_request_ctx_t *ctx) {
116+
if (ctx && ctx->status.code() == Code::UNAUTHENTICATED &&
117+
ctx->status.error_cause() == utils::Status::AUTH &&
118+
(ctx->status.message() == kInvalidAuthToken ||
119+
ctx->status.message() == kExpiredAuthToken)) {
120+
std::string url = ctx->request_handler->GetAuthorizationUrl();
121+
if (!url.empty()) {
122+
r->headers_out.status = NGX_HTTP_MOVED_TEMPORARILY;
123+
124+
ngx_table_elt_t *loc;
125+
loc = reinterpret_cast<ngx_table_elt_t *>(
126+
ngx_list_push(&r->headers_out.headers));
127+
if (loc == nullptr) {
128+
return NGX_ERROR;
129+
}
130+
131+
loc->key = kLocation;
132+
loc->lowcase_key = const_cast<u_char *>(kLocationLowcase);
133+
loc->hash = ngx_hash_key(const_cast<u_char *>(kLocationLowcase),
134+
sizeof(kLocationLowcase) - 1);
135+
136+
ngx_str_copy_from_std(r->pool, url, &loc->value);
137+
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
138+
"ESP authorization_url: %V", &loc->value);
139+
r->headers_out.location = loc;
140+
}
141+
}
142+
return NGX_OK;
143+
}
144+
105145
ngx_int_t ngx_esp_error_header_filter(ngx_http_request_t *r) {
106146
ngx_esp_request_ctx_t *ctx = reinterpret_cast<ngx_esp_request_ctx_t *>(
107147
ngx_http_get_module_ctx(r, ngx_esp_module));
@@ -130,6 +170,9 @@ ngx_int_t ngx_esp_error_header_filter(ngx_http_request_t *r) {
130170
ngx_int_t ret;
131171
ret = ngx_esp_handle_www_authenticate(r, ctx);
132172
if (ret != NGX_OK) return ret;
173+
174+
ret = ngx_esp_handle_authorization_url(r, ctx);
175+
if (ret != NGX_OK) return ret;
133176
}
134177

135178
// Clear headers (refilled by subsequent NGX header filters)

src/nginx/t/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ nginx_suite(
5757
"auth_ok_check_fail.t",
5858
"auth_pass_user_info.t",
5959
"auth_pkey_cache.t",
60+
"auth_redirect.t",
6061
"auth_remove_user_info.t",
6162
"auth_unreachable_pkey.t",
6263
"new_http.t",

src/nginx/t/auth_pkey.t

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ authentication {
5252
id: "test_auth"
5353
issuer: "628645741881-noabiu23f5a8m8ovd8ucv698lj78vv0l\@developer.gserviceaccount.com"
5454
jwks_uri: "http://127.0.0.1:${PubkeyPort}/pubkey"
55+
authorization_url: "http://dummy-redirect-url"
5556
}
5657
rules {
5758
selector: "ListShelves"

src/nginx/t/auth_redirect.t

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Copyright (C) Extensible Service Proxy Authors
2+
# All rights reserved.
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions
6+
# are met:
7+
# 1. Redistributions of source code must retain the above copyright
8+
# notice, this list of conditions and the following disclaimer.
9+
# 2. Redistributions in binary form must reproduce the above copyright
10+
# notice, this list of conditions and the following disclaimer in the
11+
# documentation and/or other materials provided with the distribution.
12+
#
13+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16+
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19+
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20+
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22+
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23+
# SUCH DAMAGE.
24+
#
25+
################################################################################
26+
#
27+
use strict;
28+
use warnings;
29+
30+
################################################################################
31+
32+
use src::nginx::t::ApiManager; # Must be first (sets up import path to the Nginx test module)
33+
use src::nginx::t::HttpServer;
34+
use src::nginx::t::ServiceControl;
35+
use src::nginx::t::Auth;
36+
use Test::Nginx; # Imports Nginx's test module
37+
use Test::More; # And the test framework
38+
39+
################################################################################
40+
41+
# Port allocations
42+
43+
my $NginxPort = ApiManager::pick_port();
44+
45+
my $t = Test::Nginx->new()->has(qw/http proxy/)->plan(5);
46+
47+
my $config = ApiManager::get_bookstore_service_config .<<"EOF";
48+
producer_project_id: "endpoints-test"
49+
authentication {
50+
providers {
51+
id: "test_auth"
52+
issuer: "628645741881-noabiu23f5a8m8ovd8ucv698lj78vv0l\@developer.gserviceaccount.com"
53+
jwks_uri: "http://127.0.0.1/pubkey"
54+
authorization_url: "http://dummy-redirect-url"
55+
}
56+
rules {
57+
selector: "ListShelves"
58+
requirements {
59+
provider_id: "test_auth"
60+
audiences: "ok_audience_1"
61+
}
62+
}
63+
}
64+
control {
65+
environment: "http://127.0.0.1:3000"
66+
}
67+
EOF
68+
$t->write_file('service.pb.txt', $config);
69+
70+
# enable the redirect_authorization_url flag
71+
ApiManager::write_file_expand($t, 'server_config.txt', <<"EOF");
72+
api_authentication_config {
73+
redirect_authorization_url: true
74+
}
75+
EOF
76+
77+
$t->write_file_expand('nginx.conf', <<"EOF");
78+
%%TEST_GLOBALS%%
79+
daemon off;
80+
events {
81+
worker_connections 32;
82+
}
83+
http {
84+
%%TEST_GLOBALS_HTTP%%
85+
server_tokens off;
86+
server {
87+
listen 127.0.0.1:${NginxPort};
88+
server_name localhost;
89+
location / {
90+
endpoints {
91+
api service.pb.txt;
92+
server_config server_config.txt;
93+
on;
94+
}
95+
proxy_pass http://127.0.0.1:3000;
96+
}
97+
}
98+
}
99+
EOF
100+
101+
$t->run();
102+
103+
################################################################################
104+
# no auth token
105+
my $response1 = ApiManager::http_get($NginxPort, "/shelves");
106+
107+
# should redirect
108+
like($response1, qr/HTTP\/1\.1 302 Moved Temporarily/, 'Returned HTTP 302.');
109+
like($response1, qr/Location: http:\/\/dummy-redirect-url/, 'Return correct redirect location.');
110+
111+
# expired auth token
112+
my $expired_token = Auth::get_expired_jwt_token;
113+
my $response2 = ApiManager::http($NginxPort, <<"EOF");
114+
GET /shelves HTTP/1.0
115+
Host: localhost
116+
Authorization: Bearer $expired_token
117+
118+
EOF
119+
120+
# should redirect
121+
like($response2, qr/HTTP\/1\.1 302 Moved Temporarily/, 'Returned HTTP 302.');
122+
like($response2, qr/Location: http:\/\/dummy-redirect-url/, 'Return correct redirect location.');
123+
124+
#invalid auth token
125+
my $response3 = ApiManager::http($NginxPort, <<"EOF");
126+
GET /shelves HTTP/1.0
127+
Host: localhost
128+
Authorization: Bearer invalid_token
129+
130+
EOF
131+
132+
# should not redirect.
133+
like($response3, qr/HTTP\/1\.1 401 Unauthorized/, 'Returned HTTP 401.');
134+
135+
$t->stop_daemons();
136+
137+
################################################################################
138+

start_esp/server-auto.conf.template

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,14 @@ service_control_config {
6767
% endif
6868
}
6969
% endif
70-
% if jwks_cache_duration_in_s:
70+
% if jwks_cache_duration_in_s or redirect_authorization_url:
7171
api_authentication_config {
7272
% if jwks_cache_duration_in_s:
7373
jwks_cache_duration_in_s: ${jwks_cache_duration_in_s}
7474
% endif
75+
% if redirect_authorization_url:
76+
redirect_authorization_url: true
77+
% endif
7578
}
7679
% endif
7780
% if client_ip_header:

start_esp/start_esp.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ def write_server_config_template(server_config_path, args):
208208
compute_platform_override=args.compute_platform_override,
209209
grpc_backend_ssl_credentials=args.grpc_backend_ssl_credentials,
210210
jwks_cache_duration_in_s=args.jwks_cache_duration_in_s,
211+
redirect_authorization_url=args.enable_jwt_authorization_url_redirect,
211212
rollout_fetch_throttle_window_in_s=args.rollout_fetch_throttle_window_in_s)
212213

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

943+
parser.add_argument('--enable_jwt_authorization_url_redirect', action='store_true',
944+
help='''If set to true, authentication failed requests will be redirected
945+
to the URL specified by the `authorizationUrl` field in OpenAPI specification.
946+
The default is false.''')
947+
942948
parser.add_argument('--rollout_fetch_throttle_window_in_s', default=None, type=int, help='''
943949
When a new rollout is detected, ESP will call ServiceManagement to get the
944950
new service configs. ServiceManagement API has a low calling quota, in order not

0 commit comments

Comments
 (0)