Skip to content

Commit 2a31b35

Browse files
shawndotkimshawnhankim
authored andcommitted
feat: access token & new endpoints (/login, /userinfo, /v2/logout)
**1. access token** - Enhance the NJS Code to capture the `access_token` sent by the IdP. - Store the `access_token` in the k/v store as same as we store `id_token` and `refresh_token` **2. new endpoints** - Add `/userinfo` endpoint: - Add a map variable of `$oidc_userinfo_endpoint` as same as authz and token endpoints here (`openid_connect_configuration.conf`) . - Expose `/userinfo` endpoint here(`openid_connect.server_conf`) in a location block of NGINX Plus to interact with IdP's `userinfo_endpoint` which is defined in the endpoint of`well-known/openid-configuration`. - The nginx location block should proxy to the IdP’s `userinfo_endpoint` by adding `access_token` as a bearer token. ``` Authorization : Bearer <access_token> ``` - The response coming from IdP should be returned back to the caller as it is. - Expose `/login` endpoint: - Expose the `/login` endpoint as a location block here (`openid_connect.server_conf`) - Proxy it to the IdP's `authorization_endpoint` configured in the map variable of `$oidc_authz_endpoint` in (`openid_connect_configuration.conf`). - This would outsource the login function to IdP as its configured. - Expose `/v2/logout` endpoint: - Expose the `/v2/logout` endpoint as a location block here (`openid_connect.server_conf`) - Add a map variable of `$oidc_end_session_endpoint` as same as authz and token endpoints here (`openid_connect_configuration.conf`) . - Proxy it to the IdP's `end_session_endpoint` to finish the session by IdP. - Expose `/v2/_logout` endpoint: - Expose `/v2/_logout` endpoint which is a callback from IdP as a location block here (`openid_connect.server_conf`) to handle the following sequences. - 1. Redirected by IdP when IdP successfully finished the session. - 2. NGINX Plus: Clear session cookies. - 3. NGINX Plus: Redirect to either the original landing page or the custom logout page by calling - Add a map of `$post_logout_return_uri`: After the successful logout from the IdP, NGINX Plus calls this URI to redirect to either the original page or a custom logout page. The default is original page based on the configuration of `$redirect_base`. **3. add endpoints in `configure.sh`** - IdP's userinfo endpoint - IdP's end session endpoint fix: enhance access-token, userinfo, logout and example
1 parent f09769b commit 2a31b35

File tree

6 files changed

+391
-102
lines changed

6 files changed

+391
-102
lines changed

README.md

Lines changed: 147 additions & 71 deletions
Large diffs are not rendered by default.

configure.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ fi
120120
# Build an intermediate configuration file
121121
# File format is: <NGINX variable name><space><IdP value>
122122
#
123-
jq -r '. | "$oidc_authz_endpoint \(.authorization_endpoint)\n$oidc_token_endpoint \(.token_endpoint)\n$oidc_jwks_uri \(.jwks_uri)"' < /tmp/${COMMAND}_$$_json > /tmp/${COMMAND}_$$_conf
123+
jq -r '. | "$oidc_authz_endpoint \(.authorization_endpoint)\n$oidc_token_endpoint \(.token_endpoint)\n$oidc_jwks_uri \(.jwks_uri)\n$oidc_end_session_endpoint \(.end_session_endpoint)\n$oidc_userinfo_endpoint \(.userinfo_endpoint)"' < /tmp/${COMMAND}_$$_json > /tmp/${COMMAND}_$$_conf
124124

125125
# Create a random value for HMAC key, adding to the intermediate configuration file
126126
echo "\$oidc_hmac_key `openssl rand -base64 18`" >> /tmp/${COMMAND}_$$_conf
@@ -178,7 +178,7 @@ fi
178178

179179
# Loop through each configuration variable
180180
echo "$COMMAND: NOTICE: Configuring $CONFDIR/openid_connect_configuration.conf"
181-
for OIDC_VAR in \$oidc_authz_endpoint \$oidc_token_endpoint \$oidc_jwt_keyfile \$oidc_hmac_key $CLIENT_ID_VAR $CLIENT_SECRET_VAR $PKCE_ENABLE_VAR; do
181+
for OIDC_VAR in \$oidc_authz_endpoint \$oidc_token_endpoint \$oidc_jwt_keyfile \$oidc_end_session_endpoint \$oidc_userinfo_endpoint \$oidc_hmac_key $CLIENT_ID_VAR $CLIENT_SECRET_VAR $PKCE_ENABLE_VAR; do
182182
# Pull the configuration value from the intermediate file
183183
VALUE=`grep "^$OIDC_VAR " /tmp/${COMMAND}_$$_conf | cut -f2 -d' '`
184184
echo -n "$COMMAND: NOTICE: - $OIDC_VAR ..."

frontend.conf

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# This is the backend application we are protecting with OpenID Connect
1+
# This is the backend site/app we are protecting with OpenID Connect
22
upstream my_backend {
33
zone my_backend 64k;
44
server 10.0.0.1:80;
@@ -15,20 +15,39 @@ server {
1515
error_log /var/log/nginx/error.log debug; # Reduce severity level as required
1616

1717
listen 8010; # Use SSL/TLS in production
18-
18+
1919
location / {
20-
# This site is protected with OpenID Connect
20+
# This site can be either directly protected with OpenID Connect or
21+
# shown with just a landing page without login.
22+
23+
# Disable when you need to show a default landing page before login.
2124
auth_jwt "" token=$session_jwt;
2225
error_page 401 = @do_oidc_flow;
26+
auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
27+
#auth_jwt_key_request /_jwks_uri; # Enable when using URL
2328

24-
auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
25-
#auth_jwt_key_request /_jwks_uri; # Enable when using URL
29+
# Successfully authenticated users are proxied to the backend site/app
30+
# with 'sub' claim passed as HTTP header. It is empty before login.
31+
proxy_set_header userid $jwt_claim_sub;
32+
33+
# The 'access_token' is set in the OIDC flow. Otherwise, it is empty.
34+
proxy_set_header Authorization "Bearer $access_token";
2635

27-
# Successfully authenticated users are proxied to the backend,
28-
# with 'sub' claim passed as HTTP header
29-
proxy_set_header username $jwt_claim_sub;
3036
proxy_pass http://my_backend; # The backend site/app
31-
37+
access_log /var/log/nginx/access.log main_jwt;
38+
}
39+
40+
location = /login {
41+
# This location can be called by SPA to start OIDC flow via login button
42+
# after starting a landing page without login.
43+
auth_jwt "" token=$session_jwt;
44+
error_page 401 = @do_oidc_flow;
45+
46+
auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
47+
#auth_jwt_key_request /_jwks_uri; # Enable when using URL
48+
49+
# Redirect to the the landing page after successful login to AS.
50+
js_content oidc.redirectPostLogin;
3251
access_log /var/log/nginx/access.log main_jwt;
3352
}
3453
}

openid_connect.js

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,18 @@
55
*/
66
var newSession = false; // Used by oidcAuth() and validateIdToken()
77

8-
export default {auth, codeExchange, validateIdToken, logout};
8+
const EXTRA_PARAMS = 1;
9+
const REPLACE_PARAMS = 2;
10+
11+
export default {
12+
auth,
13+
codeExchange,
14+
validateIdToken,
15+
logout,
16+
redirectPostLogin,
17+
redirectPostLogout,
18+
userInfo
19+
};
920

1021
function retryOriginalRequest(r) {
1122
delete r.headersOut["WWW-Authenticate"]; // Remove evidence of original failed auth_jwt
@@ -104,6 +115,11 @@ function auth(r, afterSyncCheck) {
104115
// ID Token is valid, update keyval
105116
r.log("OIDC refresh success, updating id_token for " + r.variables.cookie_auth_token);
106117
r.variables.session_jwt = tokenset.id_token; // Update key-value store
118+
if (tokenset.access_token) {
119+
r.variables.access_token = tokenset.access_token;
120+
} else {
121+
r.variables.access_token = "-";
122+
}
107123

108124
// Update refresh token (if we got a new one)
109125
if (r.variables.refresh_token != tokenset.refresh_token) {
@@ -187,6 +203,12 @@ function codeExchange(r) {
187203
// Add opaque token to keyval session store
188204
r.log("OIDC success, creating session " + r.variables.request_id);
189205
r.variables.new_session = tokenset.id_token; // Create key-value store entry
206+
if (tokenset.access_token) {
207+
r.variables.new_access_token = tokenset.access_token;
208+
} else {
209+
r.variables.new_access_token = "-";
210+
}
211+
190212
r.headersOut["Set-Cookie"] = "auth_token=" + r.variables.request_id + "; " + r.variables.oidc_cookie_flags;
191213
r.return(302, r.variables.redirect_base + r.variables.cookie_auth_redir);
192214
}
@@ -253,11 +275,31 @@ function validateIdToken(r) {
253275
}
254276
}
255277

278+
//
279+
// Default RP-Initiated or Custom Logout w/ OP.
280+
//
281+
// - An RP requests that the OP log out the end-user by redirecting the
282+
// end-user's User Agent to the OP's Logout endpoint.
283+
// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
284+
// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RedirectionAfterLogout
285+
//
256286
function logout(r) {
257287
r.log("OIDC logout for " + r.variables.cookie_auth_token);
258-
r.variables.session_jwt = "-";
259-
r.variables.refresh_token = "-";
260-
r.return(302, r.variables.oidc_logout_redirect);
288+
var idToken = r.variables.session_jwt;
289+
var queryParams = '?post_logout_redirect_uri=' +
290+
r.variables.redirect_base +
291+
r.variables.oidc_logout_redirect +
292+
'&id_token_hint=' + idToken;
293+
if (r.variables.oidc_end_session_query_params_option == REPLACE_PARAMS) {
294+
queryParams = '?' + r.variables.oidc_end_session_query_params;
295+
} else if (r.variables.oidc_end_session_query_params_option == EXTRA_PARAMS) {
296+
queryParams += '&' + r.variables.oidc_end_session_query_params;
297+
}
298+
r.variables.request_id = '-';
299+
r.variables.session_jwt = '-';
300+
r.variables.access_token = '-';
301+
r.variables.refresh_token = '-';
302+
r.return(302, r.variables.oidc_end_session_endpoint + queryParams);
261303
}
262304

263305
function getAuthZArgs(r) {
@@ -298,4 +340,59 @@ function idpClientAuth(r) {
298340
} else {
299341
return "code=" + r.variables.arg_code + "&client_secret=" + r.variables.oidc_client_secret;
300342
}
343+
}
344+
345+
//
346+
// Redirect URI after successful login from the OP.
347+
//
348+
function redirectPostLogin(r) {
349+
if (r.variables.oidc_landing_page) {
350+
r.return(302, r.variables.oidc_landing_page);
351+
} else {
352+
r.return(302, r.variables.redirect_base + r.variables.cookie_auth_redir);
353+
}
354+
}
355+
356+
//
357+
// Redirect URI after logged-out from the OP.
358+
//
359+
function redirectPostLogout(r) {
360+
r.return(302, r.variables.post_logout_return_uri);
361+
}
362+
363+
//
364+
// Return necessary user info claims after receiving and extracting all claims
365+
// that are received from the OpenID Connect Provider(OP).
366+
//
367+
function userInfo(r) {
368+
r.subrequest('/_userinfo',
369+
function(res) {
370+
if (res.status == 200) {
371+
var error_log = "OIDC userinfo JSON failure";
372+
var claimsOP = ''; // Claims that are received by the OP.
373+
try {
374+
claimsOP = JSON.parse(res.responseBody);
375+
} catch (e) {
376+
error_log += ": " + res.responseBody;
377+
r.error(error_log);
378+
r.return(500);
379+
return;
380+
}
381+
// The claimsRP is to extract claims that are configured in
382+
// $oidc_userinfo_response_data in the RP and send them to
383+
// the client using the response of the OP.
384+
var claimsRP = r.variables.oidc_userinfo_response_data.split(",");
385+
var ret = {};
386+
for (var i in claimsRP) {
387+
if (claimsRP[i] in claimsOP) {
388+
ret[claimsRP[i]] = claimsOP[claimsRP[i]];
389+
}
390+
}
391+
r.variables.user_info = JSON.stringify(ret);
392+
r.return(200, r.variables.user_info);
393+
} else {
394+
r.return(res.status)
395+
}
396+
}
397+
);
301398
}

openid_connect.server_conf

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Advanced configuration START
22
set $internal_error_message "NGINX / OpenID Connect login failure\n";
33
set $pkce_id "";
4-
resolver 8.8.8.8; # For DNS lookup of IdP endpoints;
4+
resolver 8.8.8.8; # For global DNS lookup of IDP endpoint
5+
56
subrequest_output_buffer_size 32k; # To fit a complete tokenset response
67
gunzip on; # Decompress IdP responses if necessary
78
# Advanced configuration END
@@ -42,7 +43,7 @@
4243
proxy_set_body "grant_type=authorization_code&client_id=$oidc_client&$args&redirect_uri=$redirect_base$redir_location";
4344
proxy_method POST;
4445
proxy_pass $oidc_token_endpoint;
45-
}
46+
}
4647

4748
location = /_refresh {
4849
# This location is called by oidcAuth() when performing a token refresh. We
@@ -66,17 +67,51 @@
6667
error_page 500 502 504 @oidc_error;
6768
}
6869

70+
location = /userinfo {
71+
# This location is to provide signed-in user information claims that are
72+
# defined in $oidc_userinfo_response_data.
73+
default_type application/json;
74+
if ($oidc_userinfo_response_data = '') {
75+
return 200 '{"name": "", "message":"details not provided per your policy"}';
76+
}
77+
js_content oidc.userInfo;
78+
}
79+
80+
location = /_userinfo {
81+
# This location is called by oidc.userInfo() when calling /userinfo
82+
# to get signed-in user information from the OP:
83+
# - https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
84+
internal;
85+
proxy_ssl_server_name on; # For SNI to the IdP
86+
proxy_set_header Authorization "Bearer $access_token";
87+
proxy_pass $oidc_userinfo_endpoint;
88+
}
89+
6990
location = /logout {
91+
# RP-Initiated Logout to interact with $oidc_end_session_endpoint as per:
92+
# https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
7093
status_zone "OIDC logout";
71-
add_header Set-Cookie "auth_token=; $oidc_cookie_flags"; # Send empty cookie
72-
add_header Set-Cookie "auth_redir=; $oidc_cookie_flags"; # Erase original cookie
7394
js_content oidc.logout;
7495
}
7596

7697
location = /_logout {
77-
# This location is the default value of $oidc_logout_redirect (in case it wasn't configured)
98+
# This location is a RP's callback URI that is the default value of
99+
# $oidc_logout_redirect which is called by OP after successful logout.
100+
101+
# Clean cookies
102+
add_header Set-Cookie "auth_token=; $oidc_cookie_flags"; # Send empty cookie
103+
add_header Set-Cookie "auth_redir=; $oidc_cookie_flags"; # Erase original cookie
104+
add_header Set-Cookie "auth_nonce=; $oidc_cookie_flags";
105+
106+
# Enable one of the following examples.
107+
108+
# Example 1. Built-in, simple logout page
78109
default_type text/plain;
79110
return 200 "Logged out\n";
111+
112+
# Example 2. Redirect to either the landing page or custom logout page
113+
# using the map of $post_logout_return_uri.
114+
#js_content oidc.redirectPostLogout;
80115
}
81116

82117
location @oidc_error {

openid_connect_configuration.conf

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,40 @@ map $host $oidc_jwt_keyfile {
2828
default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/certs";
2929
}
3030

31+
map $host $oidc_end_session_endpoint {
32+
default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/logout";
33+
}
34+
35+
map $host $oidc_end_session_query_params_option {
36+
# 0: default query params for the RP-initiated logout
37+
# 1: extra query params is added after the default query params
38+
# 2: replace default query params with custom query params
39+
default 0;
40+
}
41+
42+
map $host $oidc_end_session_query_params {
43+
# Each IdP use different query params of the $oidc_end_session_endpoint. For
44+
# example, The Amazon Cognito requires `client_id` and `logout_uri`. The
45+
# Auth0 requires `client_id` and `returnTo`. If this option is empty, then
46+
# `post_logout_redirect_uri` and `id_token_hint` are used as default query
47+
# params, and the AzureAD/Okta/Keycloak/OneLogin/PingIdentity use them.
48+
default "";
49+
#www.example.com "client_id=$oidc_client&logout_uri=$redirect_base/_logout";
50+
}
51+
52+
map $host $oidc_userinfo_endpoint {
53+
default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/userinfo";
54+
}
55+
56+
map $host $oidc_userinfo_response_data {
57+
# The $oidc_userinfo_endpoint returns OP's response that contains default or
58+
# customized claims. This is used for scenarios where the SPA needs to show
59+
# user name or specific profiles instead of forwarding the response from the
60+
# OP to the SPA to minimize exposure of user information.
61+
default "";
62+
#www.example.com "sub,name,preferred_username,given_name,family_name,email,photo";
63+
}
64+
3165
map $host $oidc_client {
3266
default "my-client-id";
3367
}
@@ -44,12 +78,35 @@ map $host $oidc_scopes {
4478
default "openid+profile+email+offline_access";
4579
}
4680

81+
map $host $oidc_landing_page {
82+
# Where to send browser after successful login. This option is only
83+
# recommended for scenarios where a landing page shows default information
84+
# without login, and the RP redirects to the landing page after successful
85+
# login from the OP. If this is empty, then the RP redirects to $request_uri.
86+
default "";
87+
#www.example.com $redirect_base;
88+
}
89+
4790
map $host $oidc_logout_redirect {
48-
# Where to send browser after requesting /logout location. This can be
49-
# replaced with a custom logout page, or complete URL.
91+
# This is a RP's callback URI which is called by OP after successful logout.
5092
default "/_logout"; # Built-in, simple logout page
5193
}
5294

95+
map $host $post_logout_return_uri {
96+
# Where to send browser after the RP requests /logout to the OP, and after
97+
# the RP (/_logout) is called by the OP and cleans cookies. The following
98+
# examples can be replaced with a custom logout page, or a complete URL.
99+
100+
# Example 1: Redirect to the langding page.
101+
default $oidc_landing_page;
102+
103+
# Example 2: Redirect to a custom logout page
104+
#www.example.com $redirect_base/signout;
105+
106+
# Example 3: Redirect to an another complete URL
107+
#www.example.com https://www.nginx.com;
108+
}
109+
53110
map $host $oidc_hmac_key {
54111
# This should be unique for every NGINX instance/cluster
55112
default "ChangeMe";
@@ -87,15 +144,20 @@ map $http_x_forwarded_proto $proto {
87144
proxy_cache_path /var/cache/nginx/jwk levels=1 keys_zone=jwk:64k max_size=1m;
88145

89146
# Change timeout values to at least the validity period of each token type
90-
keyval_zone zone=oidc_id_tokens:1M state=conf.d/oidc_id_tokens.json timeout=1h;
91-
keyval_zone zone=refresh_tokens:1M state=conf.d/refresh_tokens.json timeout=8h;
92-
keyval_zone zone=oidc_pkce:128K timeout=90s; # Temporary storage for PKCE code verifier.
93-
94-
keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; # Exchange cookie for JWT
95-
keyval $cookie_auth_token $refresh_token zone=refresh_tokens; # Exchange cookie for refresh token
96-
keyval $request_id $new_session zone=oidc_id_tokens; # For initial session creation
97-
keyval $request_id $new_refresh zone=refresh_tokens; # ''
98-
keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;
147+
keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h;
148+
keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h;
149+
keyval_zone zone=refresh_tokens:1M state=/var/lib/nginx/state/refresh_tokens.json timeout=8h;
150+
keyval_zone zone=oidc_pkce:128K timeout=90s; # Temporary storage for PKCE code verifier.
151+
keyval_zone zone=oidc_userinfo:128K timeout=90s; # Temporary storage for user information.
152+
153+
keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; # Exchange cookie for JWT
154+
keyval $cookie_auth_token $access_token zone=oidc_access_tokens; # Exchange cookie for access token
155+
keyval $cookie_auth_token $refresh_token zone=refresh_tokens; # Exchange cookie for refresh token
156+
keyval $request_id $new_session zone=oidc_id_tokens; # For initial session creation
157+
keyval $request_id $new_access_token zone=oidc_access_tokens;
158+
keyval $request_id $new_refresh zone=refresh_tokens;
159+
keyval $request_id $user_info zone=oidc_userinfo;
160+
keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;
99161

100162
auth_jwt_claim_set $jwt_audience aud; # In case aud is an array
101163
js_import oidc from conf.d/openid_connect.js;

0 commit comments

Comments
 (0)