Skip to content

feat: access token & enhance endpoints (/login, /userinfo, /logout) #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 147 additions & 71 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ fi
# Build an intermediate configuration file
# File format is: <NGINX variable name><space><IdP value>
#
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
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

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

# Loop through each configuration variable
echo "$COMMAND: NOTICE: Configuring $CONFDIR/openid_connect_configuration.conf"
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
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
# Pull the configuration value from the intermediate file
VALUE=`grep "^$OIDC_VAR " /tmp/${COMMAND}_$$_conf | cut -f2 -d' '`
echo -n "$COMMAND: NOTICE: - $OIDC_VAR ..."
Expand Down
37 changes: 28 additions & 9 deletions frontend.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This is the backend application we are protecting with OpenID Connect
# This is the backend site/app we are protecting with OpenID Connect
upstream my_backend {
zone my_backend 64k;
server 10.0.0.1:80;
Expand All @@ -15,20 +15,39 @@ server {
error_log /var/log/nginx/error.log debug; # Reduce severity level as required

listen 8010; # Use SSL/TLS in production

location / {
# This site is protected with OpenID Connect
# This site can be either directly protected with OpenID Connect or
# shown with just a landing page without login.

# Disable when you need to show a default landing page before login.
auth_jwt "" token=$session_jwt;
error_page 401 = @do_oidc_flow;
auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
#auth_jwt_key_request /_jwks_uri; # Enable when using URL

auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
#auth_jwt_key_request /_jwks_uri; # Enable when using URL
# Successfully authenticated users are proxied to the backend site/app
# with 'sub' claim passed as HTTP header. It is empty before login.
proxy_set_header userid $jwt_claim_sub;

# The 'access_token' is set in the OIDC flow. Otherwise, it is empty.
proxy_set_header Authorization "Bearer $access_token";

# Successfully authenticated users are proxied to the backend,
# with 'sub' claim passed as HTTP header
proxy_set_header username $jwt_claim_sub;
proxy_pass http://my_backend; # The backend site/app

access_log /var/log/nginx/access.log main_jwt;
}

location = /login {
# This location can be called by SPA to start OIDC flow via login button
# after starting a landing page without login.
auth_jwt "" token=$session_jwt;
error_page 401 = @do_oidc_flow;

auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename
#auth_jwt_key_request /_jwks_uri; # Enable when using URL

# Redirect to the the landing page after successful login to AS.
js_content oidc.redirectPostLogin;
access_log /var/log/nginx/access.log main_jwt;
}
}
Expand Down
105 changes: 101 additions & 4 deletions openid_connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@
*/
var newSession = false; // Used by oidcAuth() and validateIdToken()

export default {auth, codeExchange, validateIdToken, logout};
const EXTRA_PARAMS = 1;
const REPLACE_PARAMS = 2;

export default {
auth,
codeExchange,
validateIdToken,
logout,
redirectPostLogin,
redirectPostLogout,
userInfo
};

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

// Update refresh token (if we got a new one)
if (r.variables.refresh_token != tokenset.refresh_token) {
Expand Down Expand Up @@ -187,6 +203,12 @@ function codeExchange(r) {
// Add opaque token to keyval session store
r.log("OIDC success, creating session " + r.variables.request_id);
r.variables.new_session = tokenset.id_token; // Create key-value store entry
if (tokenset.access_token) {
r.variables.new_access_token = tokenset.access_token;
} else {
r.variables.new_access_token = "-";
}

r.headersOut["Set-Cookie"] = "auth_token=" + r.variables.request_id + "; " + r.variables.oidc_cookie_flags;
r.return(302, r.variables.redirect_base + r.variables.cookie_auth_redir);
}
Expand Down Expand Up @@ -253,11 +275,31 @@ function validateIdToken(r) {
}
}

//
// Default RP-Initiated or Custom Logout w/ OP.
//
// - An RP requests that the OP log out the end-user by redirecting the
// end-user's User Agent to the OP's Logout endpoint.
// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RedirectionAfterLogout
//
function logout(r) {
r.log("OIDC logout for " + r.variables.cookie_auth_token);
r.variables.session_jwt = "-";
r.variables.refresh_token = "-";
r.return(302, r.variables.oidc_logout_redirect);
var idToken = r.variables.session_jwt;
var queryParams = '?post_logout_redirect_uri=' +
r.variables.redirect_base +
r.variables.oidc_logout_redirect +
'&id_token_hint=' + idToken;
if (r.variables.oidc_end_session_query_params_option == REPLACE_PARAMS) {
queryParams = '?' + r.variables.oidc_end_session_query_params;
} else if (r.variables.oidc_end_session_query_params_option == EXTRA_PARAMS) {
queryParams += '&' + r.variables.oidc_end_session_query_params;
}
r.variables.request_id = '-';
r.variables.session_jwt = '-';
r.variables.access_token = '-';
r.variables.refresh_token = '-';
r.return(302, r.variables.oidc_end_session_endpoint + queryParams);
}

function getAuthZArgs(r) {
Expand Down Expand Up @@ -298,4 +340,59 @@ function idpClientAuth(r) {
} else {
return "code=" + r.variables.arg_code + "&client_secret=" + r.variables.oidc_client_secret;
}
}

//
// Redirect URI after successful login from the OP.
//
function redirectPostLogin(r) {
if (r.variables.oidc_landing_page) {
r.return(302, r.variables.oidc_landing_page);
} else {
r.return(302, r.variables.redirect_base + r.variables.cookie_auth_redir);
}
}

//
// Redirect URI after logged-out from the OP.
//
function redirectPostLogout(r) {
r.return(302, r.variables.post_logout_return_uri);
}

//
// Return necessary user info claims after receiving and extracting all claims
// that are received from the OpenID Connect Provider(OP).
//
function userInfo(r) {
r.subrequest('/_userinfo',
function(res) {
if (res.status == 200) {
var error_log = "OIDC userinfo JSON failure";
var claimsOP = ''; // Claims that are received by the OP.
try {
claimsOP = JSON.parse(res.responseBody);
} catch (e) {
error_log += ": " + res.responseBody;
r.error(error_log);
r.return(500);
return;
}
// The claimsRP is to extract claims that are configured in
// $oidc_userinfo_response_data in the RP and send them to
// the client using the response of the OP.
var claimsRP = r.variables.oidc_userinfo_response_data.split(",");
var ret = {};
for (var i in claimsRP) {
if (claimsRP[i] in claimsOP) {
ret[claimsRP[i]] = claimsOP[claimsRP[i]];
}
}
r.variables.user_info = JSON.stringify(ret);
r.return(200, r.variables.user_info);
} else {
r.return(res.status)
}
}
);
}
45 changes: 40 additions & 5 deletions openid_connect.server_conf
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Advanced configuration START
set $internal_error_message "NGINX / OpenID Connect login failure\n";
set $pkce_id "";
resolver 8.8.8.8; # For DNS lookup of IdP endpoints;
resolver 8.8.8.8; # For global DNS lookup of IDP endpoint

subrequest_output_buffer_size 32k; # To fit a complete tokenset response
gunzip on; # Decompress IdP responses if necessary
# Advanced configuration END
Expand Down Expand Up @@ -42,7 +43,7 @@
proxy_set_body "grant_type=authorization_code&client_id=$oidc_client&$args&redirect_uri=$redirect_base$redir_location";
proxy_method POST;
proxy_pass $oidc_token_endpoint;
}
}

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

location = /userinfo {
# This location is to provide signed-in user information claims that are
# defined in $oidc_userinfo_response_data.
default_type application/json;
if ($oidc_userinfo_response_data = '') {
return 200 '{"name": "", "message":"details not provided per your policy"}';
}
js_content oidc.userInfo;
}

location = /_userinfo {
# This location is called by oidc.userInfo() when calling /userinfo
# to get signed-in user information from the OP:
# - https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
internal;
proxy_ssl_server_name on; # For SNI to the IdP
proxy_set_header Authorization "Bearer $access_token";
proxy_pass $oidc_userinfo_endpoint;
}

location = /logout {
# RP-Initiated Logout to interact with $oidc_end_session_endpoint as per:
# https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
status_zone "OIDC logout";
add_header Set-Cookie "auth_token=; $oidc_cookie_flags"; # Send empty cookie
add_header Set-Cookie "auth_redir=; $oidc_cookie_flags"; # Erase original cookie
js_content oidc.logout;
}

location = /_logout {
# This location is the default value of $oidc_logout_redirect (in case it wasn't configured)
# This location is a RP's callback URI that is the default value of
# $oidc_logout_redirect which is called by OP after successful logout.

# Clean cookies
add_header Set-Cookie "auth_token=; $oidc_cookie_flags"; # Send empty cookie
add_header Set-Cookie "auth_redir=; $oidc_cookie_flags"; # Erase original cookie
add_header Set-Cookie "auth_nonce=; $oidc_cookie_flags";

# Enable one of the following examples.

# Example 1. Built-in, simple logout page
default_type text/plain;
return 200 "Logged out\n";

# Example 2. Redirect to either the landing page or custom logout page
# using the map of $post_logout_return_uri.
#js_content oidc.redirectPostLogout;
}

location @oidc_error {
Expand Down
84 changes: 73 additions & 11 deletions openid_connect_configuration.conf
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,40 @@ map $host $oidc_jwt_keyfile {
default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/certs";
}

map $host $oidc_end_session_endpoint {
default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/logout";
}

map $host $oidc_end_session_query_params_option {
# 0: default query params for the RP-initiated logout
# 1: extra query params is added after the default query params
# 2: replace default query params with custom query params
default 0;
}

map $host $oidc_end_session_query_params {
# Each IdP use different query params of the $oidc_end_session_endpoint. For
# example, The Amazon Cognito requires `client_id` and `logout_uri`. The
# Auth0 requires `client_id` and `returnTo`. If this option is empty, then
# `post_logout_redirect_uri` and `id_token_hint` are used as default query
# params, and the AzureAD/Okta/Keycloak/OneLogin/PingIdentity use them.
default "";
#www.example.com "client_id=$oidc_client&logout_uri=$redirect_base/_logout";
}

map $host $oidc_userinfo_endpoint {
default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/userinfo";
}

map $host $oidc_userinfo_response_data {
# The $oidc_userinfo_endpoint returns OP's response that contains default or
# customized claims. This is used for scenarios where the SPA needs to show
# user name or specific profiles instead of forwarding the response from the
# OP to the SPA to minimize exposure of user information.
default "";
#www.example.com "sub,name,preferred_username,given_name,family_name,email,photo";
}

map $host $oidc_client {
default "my-client-id";
}
Expand All @@ -44,12 +78,35 @@ map $host $oidc_scopes {
default "openid+profile+email+offline_access";
}

map $host $oidc_landing_page {
# Where to send browser after successful login. This option is only
# recommended for scenarios where a landing page shows default information
# without login, and the RP redirects to the landing page after successful
# login from the OP. If this is empty, then the RP redirects to $request_uri.
default "";
#www.example.com $redirect_base;
}

map $host $oidc_logout_redirect {
# Where to send browser after requesting /logout location. This can be
# replaced with a custom logout page, or complete URL.
# This is a RP's callback URI which is called by OP after successful logout.
default "/_logout"; # Built-in, simple logout page
}

map $host $post_logout_return_uri {
# Where to send browser after the RP requests /logout to the OP, and after
# the RP (/_logout) is called by the OP and cleans cookies. The following
# examples can be replaced with a custom logout page, or a complete URL.

# Example 1: Redirect to the langding page.
default $oidc_landing_page;

# Example 2: Redirect to a custom logout page
#www.example.com $redirect_base/signout;

# Example 3: Redirect to an another complete URL
#www.example.com https://www.nginx.com;
}

map $host $oidc_hmac_key {
# This should be unique for every NGINX instance/cluster
default "ChangeMe";
Expand Down Expand Up @@ -87,15 +144,20 @@ map $http_x_forwarded_proto $proto {
proxy_cache_path /var/cache/nginx/jwk levels=1 keys_zone=jwk:64k max_size=1m;

# Change timeout values to at least the validity period of each token type
keyval_zone zone=oidc_id_tokens:1M state=conf.d/oidc_id_tokens.json timeout=1h;
keyval_zone zone=refresh_tokens:1M state=conf.d/refresh_tokens.json timeout=8h;
keyval_zone zone=oidc_pkce:128K timeout=90s; # Temporary storage for PKCE code verifier.

keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; # Exchange cookie for JWT
keyval $cookie_auth_token $refresh_token zone=refresh_tokens; # Exchange cookie for refresh token
keyval $request_id $new_session zone=oidc_id_tokens; # For initial session creation
keyval $request_id $new_refresh zone=refresh_tokens; # ''
keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;
keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h;
keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h;
keyval_zone zone=refresh_tokens:1M state=/var/lib/nginx/state/refresh_tokens.json timeout=8h;
keyval_zone zone=oidc_pkce:128K timeout=90s; # Temporary storage for PKCE code verifier.
keyval_zone zone=oidc_userinfo:128K timeout=90s; # Temporary storage for user information.

keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; # Exchange cookie for JWT
keyval $cookie_auth_token $access_token zone=oidc_access_tokens; # Exchange cookie for access token
keyval $cookie_auth_token $refresh_token zone=refresh_tokens; # Exchange cookie for refresh token
keyval $request_id $new_session zone=oidc_id_tokens; # For initial session creation
keyval $request_id $new_access_token zone=oidc_access_tokens;
keyval $request_id $new_refresh zone=refresh_tokens;
keyval $request_id $user_info zone=oidc_userinfo;
keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;

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