Skip to content

Commit af1aadd

Browse files
committed
fix: enhance access-token, userinfo, logout and example
1 parent ac60940 commit af1aadd

File tree

4 files changed

+182
-285
lines changed

4 files changed

+182
-285
lines changed

frontend.conf

Lines changed: 24 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
1-
# -----------------------------------------------------------------------------#
2-
# #
3-
# Sample Reverse Proxy Configuration: Frontend Site, Backend App #
4-
# (for Open ID Connect workflow) #
5-
# #
6-
# -----------------------------------------------------------------------------#
7-
8-
# -----------------------------------------------------------------------------#
9-
# #
10-
# 1. Basic Example: Landing page starts OIDC workflow w/o login/logout button. #
11-
# #
12-
# -----------------------------------------------------------------------------#
13-
14-
# This is the backend application we are protecting with OpenID Connect
1+
# This is the backend site/application we are protecting with OpenID Connect
152
upstream my_backend {
16-
zone my_backend 64k;
3+
zone my_frontend_site 64k;
174
server 10.0.0.1:80;
185
}
196

@@ -28,122 +15,39 @@ server {
2815
error_log /var/log/nginx/error.log debug; # Reduce severity level as required
2916

3017
listen 8010; # Use SSL/TLS in production
31-
18+
3219
location / {
33-
# 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.
3424
auth_jwt "" token=$session_jwt;
3525
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
3628

37-
auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
38-
#auth_jwt_key_request /_jwks_uri; # Enable when using URL
39-
40-
# Successfully authenticated users are proxied to the backend,
41-
# with 'sub' claim passed as HTTP header
42-
proxy_set_header username $jwt_claim_sub;
43-
proxy_pass http://my_backend; # The backend site/app
44-
45-
access_log /var/log/nginx/access.log main_jwt;
46-
}
47-
}
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;
4832

49-
# -----------------------------------------------------------------------------#
50-
# #
51-
# 2. Advanced Example: Landing page, login/logout button to handle OIDC kflow #
52-
# #
53-
# - Landing page shows 'login' button #
54-
# - 'login' button calls `/login` endpoint to start OIDC flow by validating
55-
# 'id_token' w/ IdP's JWK. #
56-
# - Landing page calls `/userinfo` to show user info using 'access_token`. #
57-
# - 'logout' button to be finished OIDC session by IdP. #
58-
# - API authorization by validating `access_token` w/ IdP's JWK #
59-
# #
60-
# -----------------------------------------------------------------------------#
61-
62-
#
63-
# Upstream server for proxing to the frontend site.
64-
# - Example of a bundle frontend app to locally test NGINX Plus OIDC workflow.
65-
# https://github.com/nginx-openid-connect/nginx-oidc-examples/blob/main/001-oidc-local-test/docker/build-context/nginx/sample/proxy_server_frontend.conf
66-
# This link is subject to change.
67-
# - Modify this configuration to match your frontend site.
68-
#
69-
upstream my_frontend_site {
70-
zone my_frontend_site 64k;
71-
server 127.0.0.1:9091;
72-
}
73-
74-
#
75-
# Upstream sample for proxing to the backend API server.
76-
# - Example of a bundle backend app to locally test an API using access token.
77-
# + https://github.com/nginx-openid-connect/nginx-oidc-examples/blob/main/001-oidc-local-test/docker/build-context/nginx/sample/proxy_server_backend.conf
78-
# This link is subject to change.
79-
# - Modify this configuration to match your backend app.
80-
#
81-
upstream my_backend_app {
82-
zone my_backend_app 64k;
83-
server 127.0.0.1:9092;
84-
}
85-
86-
#
87-
# Sample Frontend-site & backend-api-server for the OIDC workflow.
88-
#
89-
server {
90-
# Enable when debugging is needed.
91-
error_log /var/log/nginx/error.log debug; # Reduce severity level as required
92-
access_log /var/log/nginx/access.log main;
93-
94-
# Replace the following server name with your host name.
95-
#
96-
# [Example: if you want to locally test OIDC in your laptop]
97-
# - Add '127.0.0.1 nginx.oidc.test` in your `/etc/hosts'.
98-
# - Use the command like 'make start'.
99-
# - Type 'https://nginx.oidc.test' in your browser.
100-
# - You will see the sample landing page and 'Sign In' button.
101-
#
102-
listen 8020; # Use SSL/TLS in production
103-
server_name nginx.oidc.test;
104-
105-
# Replace the following files with your certificate.
106-
ssl_certificate /etc/ssl/nginx/nginx-repo.crt;
107-
ssl_certificate_key /etc/ssl/nginx/nginx-repo.key;
108-
109-
# OIDC workflow
110-
include conf.d/openid_connect.server_conf;
33+
# The 'access_token' is set in the OIDC flow. Otherwise, it is empty.
34+
proxy_set_header Authorization "Bearer $access_token";
11135

112-
#
113-
# Frontend example:
114-
#
115-
# - Default landing page: no need OIDC workflow to show 'Sign In' button.
116-
# - The site is protected with OpenID Connect(OIDC) by calling the API
117-
# endpoint of `/login` when users click 'login' button.
118-
#
119-
location / {
120-
proxy_pass http://my_frontend_site;
36+
proxy_pass http://my_backend; # The frontend landing page
12137
access_log /var/log/nginx/access.log main_jwt;
12238
}
12339

124-
#
125-
# Backend API example to interact with proxied backend service:
126-
#
127-
# - This API resource is protected by access token which is received by IdP
128-
# after successful signing-in among the frontend site, NGINX Plus and IdP.
129-
#
130-
# - To ensure that client requests access the API securely, access token is
131-
# used for API authorization.
132-
# + Most of IdP generate an access token for API authorization of IdP's
133-
# endpoints (like /userinfo) as well as customer's endpoints.
134-
# + But Azure AD generate two types of access token for API authorization
135-
# of Microsoft graph API endpoints and customers' endpoints.
136-
# + Therefore, we recommend that you use $session_jwt for Azure AD and
137-
# $access_token for most of IdPs such as Cognito, Auth0, Keycloak, Okta,
138-
# OneLogin, Ping Identity, etc as for now.
139-
#
140-
location /v1/api/example {
141-
auth_jwt "" token=$access_token; # Use $session_jwt for Azure AD
142-
auth_jwt_key_request /_jwks_uri; # Enable when using URL
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+
14346
#auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
47+
auth_jwt_key_request /_jwks_uri; # Enable when using URL
14448

145-
proxy_set_header Authorization "Bearer $access_token";
146-
proxy_pass http://my_backend_app;
49+
# Redirect to the the landing page after successful login to AS.
50+
js_content oidc.redirectPostLogin;
14751
access_log /var/log/nginx/access.log main_jwt;
14852
}
14953
}

openid_connect.js

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

8+
const EXTRA_PARAMS = 1;
9+
const REPLACE_PARAMS = 2;
10+
811
export default {
912
auth,
1013
codeExchange,
1114
validateIdToken,
1215
logout,
13-
v2logout,
1416
redirectPostLogin,
15-
redirectPostLogout
17+
redirectPostLogout,
18+
userInfo
1619
};
1720

1821
function retryOriginalRequest(r) {
@@ -112,7 +115,11 @@ function auth(r, afterSyncCheck) {
112115
// ID Token is valid, update keyval
113116
r.log("OIDC refresh success, updating id_token for " + r.variables.cookie_auth_token);
114117
r.variables.session_jwt = tokenset.id_token; // Update key-value store
115-
r.variables.access_token = tokenset.access_token;
118+
if (tokenset.access_token) {
119+
r.variables.access_token = tokenset.access_token;
120+
} else {
121+
r.variables.access_token = "-";
122+
}
116123

117124
// Update refresh token (if we got a new one)
118125
if (r.variables.refresh_token != tokenset.refresh_token) {
@@ -196,7 +203,12 @@ function codeExchange(r) {
196203
// Add opaque token to keyval session store
197204
r.log("OIDC success, creating session " + r.variables.request_id);
198205
r.variables.new_session = tokenset.id_token; // Create key-value store entry
199-
r.variables.new_access_token = tokenset.access_token;
206+
if (tokenset.access_token) {
207+
r.variables.new_access_token = tokenset.access_token;
208+
} else {
209+
r.variables.new_access_token = "-";
210+
}
211+
200212
r.headersOut["Set-Cookie"] = "auth_token=" + r.variables.request_id + "; " + r.variables.oidc_cookie_flags;
201213
r.return(302, r.variables.redirect_base + r.variables.cookie_auth_redir);
202214
}
@@ -263,12 +275,31 @@ function validateIdToken(r) {
263275
}
264276
}
265277

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+
//
266286
function logout(r) {
267287
r.log("OIDC logout for " + r.variables.cookie_auth_token);
268-
r.variables.session_jwt = "-";
269-
r.variables.access_token = "-";
270-
r.variables.refresh_token = "-";
271-
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);
272303
}
273304

274305
function getAuthZArgs(r) {
@@ -312,66 +343,56 @@ function idpClientAuth(r) {
312343
}
313344

314345
//
315-
// Redirect URI after logging in the IDP.
316-
function redirectPostLogin(r) {
317-
r.return(302, r.variables.redirect_base + getIDTokenArgsAfterLogin(r));
318-
}
319-
320-
//
321-
// Get query parameter of ID token after sucessful login:
322-
//
323-
// - For the variable of `returnTokenToClientOnLogin` of the APIM, this config
324-
// is only effective for /login endpoint. By default, our implementation MUST
325-
// not return any token back to the client app.
326-
// - If its configured it can send id_token in the request uri as
327-
// `?id_token=sdfsdfdsfs` after successful login.
328-
//
346+
// Redirect URI after successful login from the OP.
329347
//
330-
function getIDTokenArgsAfterLogin(r) {
331-
if (r.variables.return_token_to_client_on_login == 'id_token') {
332-
return '?id_token=' + r.variables.id_token;
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);
333353
}
334-
return '';
335-
}
336-
337-
//
338-
// RP-Initiated or Custom Logout w/ Idp.
339-
//
340-
// - An RP requests that the Idp log out the end-user by redirecting the
341-
// end-user's User Agent to the Idp's Logout endpoint.
342-
// - TODO: Handle custom logout parameters if Idp doesn't support standard spec
343-
// of 'OpenID Connect RP-Initiated Logout 1.0'.
344-
//
345-
// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
346-
// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RedirectionAfterLogout
347-
//
348-
function v2logout(r) {
349-
r.log("OIDC logout for " + r.variables.cookie_auth_token);
350-
var idToken = r.variables.session_jwt;
351-
var queryParams = getRPInitiatedLogoutArgs(r, idToken);
352-
353-
r.variables.request_id = '-';
354-
r.variables.session_jwt = '-';
355-
r.variables.access_token = '-';
356-
r.variables.refresh_token = '-';
357-
r.return(302, r.variables.oidc_end_session_endpoint + queryParams);
358354
}
359355

360356
//
361-
// Get query params for RP-initiated logout:
357+
// Redirect URI after logged-out from the OP.
362358
//
363-
// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
364-
// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RedirectionAfterLogout
365-
//
366-
function getRPInitiatedLogoutArgs(r, idToken) {
367-
return '?post_logout_redirect_uri=' + r.variables.redirect_base
368-
+ r.variables.oidc_logout_redirect_uri +
369-
'&id_token_hint=' + idToken;
359+
function redirectPostLogout(r) {
360+
r.return(302, r.variables.post_logout_return_uri);
370361
}
371362

372363
//
373-
// Redirect URI after logged-out from the IDP.
364+
// Return necessary user info claims after receiving and extracting all claims
365+
// that are received from the OpenID Connect Provider(OP).
374366
//
375-
function redirectPostLogout(r) {
376-
r.return(302, r.variables.post_logout_return_uri);
377-
}
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+
);
398+
}

0 commit comments

Comments
 (0)