From b45807d4510c126598f98ac5b98840664001dae0 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Mon, 8 Sep 2025 12:47:49 -0500 Subject: [PATCH 1/3] Configure TLS trust and DNS resolver in http.conf --- nginx-with-github-auth/http.conf | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/nginx-with-github-auth/http.conf b/nginx-with-github-auth/http.conf index 5dc37e8..ed7483b 100644 --- a/nginx-with-github-auth/http.conf +++ b/nginx-with-github-auth/http.conf @@ -2,3 +2,13 @@ proxy_cache_path /var/cache/nginx/ouath2 levels=1 keys_zone=ouath2:1m max_size=10m; js_import nginx-with-github-auth/oauth2.js; + +# njs (ngx.fetch) TLS trust — Alpine bundle path +js_fetch_trusted_certificate /etc/ssl/cert.pem; + +js_fetch_verify on; +js_fetch_verify_depth 5; + +# Single DNS resolver for outbound lookups +resolver 1.1.1.1 8.8.8.8 valid=300s; +resolver_timeout 5s; From c0ea0d993e67ff20ba7bc2f21209126ec3f7055f Mon Sep 17 00:00:00 2001 From: alec_dev Date: Mon, 8 Sep 2025 12:52:41 -0500 Subject: [PATCH 2/3] Refactor GitHub OAuth2 authentication logic --- nginx-with-github-auth/oauth2.js | 275 ++++++++++++++++++------------- 1 file changed, 162 insertions(+), 113 deletions(-) diff --git a/nginx-with-github-auth/oauth2.js b/nginx-with-github-auth/oauth2.js index 0d505a7..fb598e2 100644 --- a/nginx-with-github-auth/oauth2.js +++ b/nginx-with-github-auth/oauth2.js @@ -1,136 +1,179 @@ /** - * Verify the GitHub OAuth 2.0 token and make sure it belongs to a user that - * is member of the organization - * - * Note: Nginx JavaScript does not support many of the ES6 features, so we - * are restricted to using ES5 syntax + * GitHub OAuth2 for Nginx (njs) — direct HTTP via ngx.fetch + * - Exchanges ?code=… for access_token + * - Verifies org membership via GraphQL + * ES5 syntax where possible for compatibility. */ function authenticate(r) { var code = getCode(r); + if (typeof code === 'string') { + requestToken(r, code); + } else { + verifyToken(r, r.variables.cookie_token); + } - if (typeof code === 'string') requestToken(code); - else verifyToken(r.variables.cookie_token); - - /** Extract "code" query string argument from GitHub sign in page */ + /** Extract "code" from the original request URI */ function getCode() { - var requestUrl = r.variables.auth_request_uri; - var queryString = requestUrl.split('?')[1]; - if (queryString === undefined) return undefined; + var requestUrl = r.variables.auth_request_uri || ''; + var qIdx = requestUrl.indexOf('?'); + if (qIdx === -1) return undefined; + + var queryString = requestUrl.substring(qIdx + 1); + if (!queryString) return undefined; + var parts = queryString.split('&'); - if (parts === undefined) return undefined; - var code = undefined; - parts.forEach(function (part) { - var array = part.split('='); - var key = array[0]; - var value = array[1]; - if (key === 'code') code = value; - }); - return code; + var i, kv, key, val; + for (i = 0; i < parts.length; i++) { + kv = parts[i].split('='); + if (kv.length !== 2) continue; + key = kv[0]; + val = kv[1]; + if (key === 'code') { + return decodeURIComponent(String(val || '').replace(/\+/g, ' ')); + } + } + return undefined; } - /** Turn a "code" query string argument into an authentication token */ - function requestToken(code) { - r.subrequest( - '/_oauth2_send_login_request', - 'code=' + code, - function (reply) { - if (reply.status !== 200) - return error( - 'OAuth unexpected response from authorization server (HTTP ' + - reply.status + - '). ' + - reply.responseBody, - 500 - ); - - // We have a response from authorization server, validate it has expected JSON schema - try { - // Test for valid JSON so that we only store good responses - var response = JSON.parse(reply.responseBody); - if (response.error !== undefined) - return error( - response.error + '\n' + response.error_description, - 500 - ); - verifyToken(response.access_token); - } catch (e) { - return error( - 'OAuth token response is not JSON: ' + reply.responseBody, - 500 - ); - } - } - ); + /** Utility: form-encode key/value object */ + function formEncode(obj) { + var out = []; + for (var k in obj) { + if (!obj.hasOwnProperty(k)) continue; + out.push(encodeURIComponent(k) + '=' + encodeURIComponent(String(obj[k]))); + } + return out.join('&'); } - function error(error, httpCode) { - if (httpCode === undefined) httpCode = 401; - r.error(error); - r.return(httpCode); + /** Exchange authorization code for access_token using ngx.fetch */ + function requestToken(r, code) { + var clientId = r.variables.oauth_client_id; + var clientSecret = r.variables.oauth_client_secret; + + if (!clientId || !clientSecret) { + return error('Missing oauth_client_id or oauth_client_secret variables', 500); + } + + var body = formEncode({ + client_id: clientId, + client_secret: clientSecret, + code: code + // redirect_uri not required when single fixed callback matches app settings + }); + + ngx.fetch('http://github.com/service/https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': 'nginx' + }, + body: body + }) + .then(function(resp) { + if (!resp.ok) { + return resp.text().then(function(text) { + error('Token exchange failed HTTP ' + resp.status + ': ' + text, 500); + }); + } + return resp.json().then(function(data) { + if (data && data.error !== undefined) { + return error(String(data.error) + (data.error_description ? '\n' + String(data.error_description) : ''), 500); + } + if (!data || !data.access_token) { + return error('OAuth token response missing access_token', 500); + } + verifyToken(r, data.access_token); + }); + }) + .catch(function(e) { + error('Token exchange exception: ' + e, 500); + }); } - /** - * Fetch list of teams and members of the organization - * If fails, it means the user is not part of the organization - */ - function verifyToken(token) { + /** Verify org membership via GitHub GraphQL using ngx.fetch */ + function verifyToken(r, token) { if (typeof token !== 'string' || token.length === 0) return r.return(401); - r.subrequest( - '/_oauth2_send_organization_info_request', - { - method: 'POST', - args: 'token=' + token, - body: JSON.stringify({ - query: - '{\norganization(login: "' + - r.variables.github_organization + - '") {\nteams(first: 40) {\nnodes {\nname\nmembers(first: 40) {\nnodes {\nlogin\n}\n}\n}\n}\n}\nviewer {\nname\nlogin\n}\n}', - }), + var org = r.variables.github_organization; + if (!org) { + return error('Missing github_organization variable', 500); + } + + var query = + '{\n' + + ' organization(login: "' + org + '") {\n' + + ' teams(first: 40) {\n' + + ' nodes {\n' + + ' name\n' + + ' members(first: 40) { nodes { login } }\n' + + ' }\n' + + ' }\n' + + ' }\n' + + ' viewer { name login }\n' + + '}'; + + ngx.fetch('http://github.com/service/https://api.github.com/graphql', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token, + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'User-Agent': 'nginx' }, - function (reply) { - try { - // Test for valid JSON so that we only store good responses - var response = JSON.parse(reply.responseBody); - var teams = {}; - response.data.organization.teams.nodes.forEach(function (node) { - teams[node.name] = node.members.nodes.map(function (node) { - return node.login; - }); - }); - - if (Object.keys(teams).length === 0) - return error( - 'Not a member of ' + - r.variables.github_organization + - ' organization', - 403 - ); - r.log( - 'OAuth2 Authentication successful. GitHub Login: ' + - response.data.viewer.login - ); - tokenResult({ - token: token, - name: response.data.viewer.name, - login: response.data.viewer.login, - organization: { - teams: teams, - }, - }); - } catch (e) { - return error( - 'OAuth token introspection response is not JSON: ' + - reply.responseBody, - 500 - ); - } + body: JSON.stringify({ query: query }) + }) + .then(function(resp) { + if (!resp.ok) { + return resp.text().then(function(text) { + error('Org check failed HTTP ' + resp.status + ': ' + text, 500); + }); } - ); + return resp.json().then(function(data) { + if (!data || data.errors) { + return error('GraphQL error: ' + JSON.stringify(data && data.errors ? data.errors : data), 500); + } + + var orgNode = data.data && data.data.organization; + var viewer = data.data && data.data.viewer; + if (!orgNode || !orgNode.teams || !orgNode.teams.nodes) { + return error('Organization info missing in GraphQL response: ' + JSON.stringify(data), 403); + } + + // Build simple map of team -> [logins] + var teamsArr = orgNode.teams.nodes || []; + var teams = {}; + for (var i = 0; i < teamsArr.length; i++) { + var t = teamsArr[i]; + var members = (t.members && t.members.nodes) || []; + var logins = []; + for (var j = 0; j < members.length; j++) { + logins.push(members[j].login); + } + teams[t.name] = logins; + } + + if (!Object.keys(teams).length) { + return error('Not a member of ' + org + ' organization', 403); + } + + r.log('OAuth2 Authentication successful. GitHub Login: ' + (viewer && viewer.login ? viewer.login : '(unknown)')); + + tokenResult(r, { + token: token, + name: viewer && viewer.name, + login: viewer && viewer.login, + organization: { teams: teams } + }); + }); + }) + .catch(function(e) { + error('Org check exception: ' + e, 500); + }); } - function tokenResult(response) { + function tokenResult(r, response) { // Check for validation success // Iterate over all members of the response and return them as response headers r.headersOut['token'] = response.token; @@ -139,6 +182,12 @@ function authenticate(r) { r.sendHeader(); r.finish(); } + + function error(msg, httpCode) { + if (httpCode === undefined) httpCode = 401; + r.error(msg); + r.return(httpCode); + } } export default { authenticate }; From 2b4405ecbaa67603558267a42b3783a3981fd590 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Mon, 8 Sep 2025 12:55:46 -0500 Subject: [PATCH 3/3] Enhance GitHub OAuth configuration in server.conf --- nginx-with-github-auth/server.conf | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/nginx-with-github-auth/server.conf b/nginx-with-github-auth/server.conf index 150a1b9..8184ff5 100644 --- a/nginx-with-github-auth/server.conf +++ b/nginx-with-github-auth/server.conf @@ -13,14 +13,28 @@ location = /_oauth2_send_login_request { internal; gunzip on; # Decompress if necessary - proxy_set_header Content-Type "application/json"; - proxy_set_header Accept "application/json"; - proxy_set_header User-Agent "nginx"; proxy_method POST; - # See https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#2-users-are-redirected-back-to-your-site-by-github -# proxy_pass "https://github.com/login/oauth/access_token/$is_args$args"; -# proxy_pass "https://files.specifysoftware.org/some/path?client_id=$oauth_client_id&client_secret=$oauth_client_secret&code=$arg_code"; - proxy_pass "https://github.com/login/oauth/access_token?client_id=$oauth_client_id&client_secret=$oauth_client_secret&code=$arg_code"; + proxy_http_version 1.1; + proxy_pass https://github.com/login/oauth/access_token; + + # REQUIRED headers/body for GitHub OAuth token endpoint + proxy_set_header Host github.com; + proxy_set_header Content-Type application/x-www-form-urlencoded; + proxy_set_header Accept application/json; + proxy_set_header User-Agent nginx; + + # Send form-encoded body (no query string) + proxy_set_body "client_id=$oauth_client_id&client_secret=$oauth_client_secret&code=$arg_code"; + + # Ensure Nginx computes the correct Content-Length for the rewritten body + proxy_pass_request_body on; + proxy_set_header Content-Length ""; + + # TLS/SNI + proxy_ssl_server_name on; + + # (Optional) give GitHub time to reply + proxy_read_timeout 30s; } location = /_oauth2_send_organization_info_request { @@ -45,4 +59,4 @@ location @autherror { # If the user is not logged in, redirect them to GitHub's login URL # See https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#1-request-a-users-github-identity return 302 "https://github.com/login/oauth/authorize?client_id=$oauth_client_id&scope=$github_scopes"; -} \ No newline at end of file +}