Skip to content

Commit 2638347

Browse files
authored
feat(accesstoken): add auth_url opts (#34)
* feat(accesstoken): add auth_url opts
1 parent 995dcd4 commit 2638347

File tree

2 files changed

+273
-17
lines changed

2 files changed

+273
-17
lines changed

spec/03-credentials_spec.lua

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,4 +243,254 @@ describe("access token", function ()
243243
end)
244244
end)
245245

246+
-- Tests for custom URL configuration
247+
describe("custom URL configuration", function()
248+
249+
it("should use custom oauth_token_url for Service Account authentication", function()
250+
local custom_oauth_url = "https://private.googleapis.com/oauth2/v4/token"
251+
local custom_responses = {
252+
[custom_oauth_url] = {
253+
status = 200,
254+
body = [[{"access_token": "test_custom_oauth_token", "expires_in": 3600}]],
255+
},
256+
["http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"] = function()
257+
return nil, "connection refused"
258+
end
259+
}
260+
261+
with_http_mock(custom_responses, function(temp_access_token)
262+
local gcpToken = temp_access_token(nil, {
263+
auth_method_order = "adc",
264+
oauth_token_url = custom_oauth_url
265+
})
266+
267+
assert.same(gcpToken.token, "test_custom_oauth_token")
268+
assert.same(gcpToken.authMethod, "SA")
269+
assert.same(gcpToken.oauthTokenUrl, custom_oauth_url)
270+
assert.is_number(gcpToken.expireTime)
271+
assert.is_false(gcpToken:needsRefresh())
272+
end)
273+
end)
274+
275+
it("should use custom metadata_url for Workload Identity authentication", function()
276+
local custom_metadata_url = "http://custom-metadata.internal/token"
277+
local custom_responses = {
278+
["https://www.googleapis.com/oauth2/v4/token"] = {
279+
status = 400,
280+
body = [[{"error": "invalid_grant"}]],
281+
},
282+
[custom_metadata_url] = {
283+
status = 200,
284+
body = [[{"access_token": "test_custom_wi_token", "expires_in": 3600}]],
285+
}
286+
}
287+
288+
with_http_mock(custom_responses, function(temp_access_token)
289+
local gcpToken = temp_access_token(nil, {
290+
auth_method_order = "legacy",
291+
metadata_url = custom_metadata_url
292+
})
293+
294+
assert.same(gcpToken.token, "test_custom_wi_token")
295+
assert.same(gcpToken.authMethod, "WI")
296+
assert.same(gcpToken.metadataUrl, custom_metadata_url)
297+
assert.is_number(gcpToken.expireTime)
298+
assert.is_false(gcpToken:needsRefresh())
299+
end)
300+
end)
301+
302+
it("should use both custom oauth_token_url and metadata_url", function()
303+
local custom_oauth_url = "https://restricted.googleapis.com/oauth2/v4/token"
304+
local custom_metadata_url = "http://private-metadata.internal/token"
305+
local custom_responses = {
306+
[custom_oauth_url] = {
307+
status = 200,
308+
body = [[{"access_token": "test_custom_both_token", "expires_in": 3600}]],
309+
},
310+
[custom_metadata_url] = {
311+
status = 200,
312+
body = [[{"access_token": "test_custom_wi_both_token", "expires_in": 3600}]],
313+
}
314+
}
315+
316+
with_http_mock(custom_responses, function(temp_access_token)
317+
-- Test with ADC (SA first)
318+
local gcpTokenSA = temp_access_token(nil, {
319+
auth_method_order = "adc",
320+
oauth_token_url = custom_oauth_url,
321+
metadata_url = custom_metadata_url
322+
})
323+
324+
assert.same(gcpTokenSA.token, "test_custom_both_token")
325+
assert.same(gcpTokenSA.authMethod, "SA")
326+
assert.same(gcpTokenSA.oauthTokenUrl, custom_oauth_url)
327+
assert.same(gcpTokenSA.metadataUrl, custom_metadata_url)
328+
end)
329+
330+
with_http_mock(custom_responses, function(temp_access_token)
331+
-- Test with legacy (WI first)
332+
local gcpTokenWI = temp_access_token(nil, {
333+
auth_method_order = "legacy",
334+
oauth_token_url = custom_oauth_url,
335+
metadata_url = custom_metadata_url
336+
})
337+
338+
assert.same(gcpTokenWI.token, "test_custom_wi_both_token")
339+
assert.same(gcpTokenWI.authMethod, "WI")
340+
assert.same(gcpTokenWI.oauthTokenUrl, custom_oauth_url)
341+
assert.same(gcpTokenWI.metadataUrl, custom_metadata_url)
342+
end)
343+
end)
344+
345+
it("should use custom oauth_token_url during SA token refresh", function()
346+
local custom_oauth_url = "https://private.googleapis.com/oauth2/v4/token"
347+
local refresh_counter = 0
348+
local custom_responses = {
349+
[custom_oauth_url] = function()
350+
refresh_counter = refresh_counter + 1
351+
return {
352+
status = 200,
353+
body = string.format([[{"access_token": "test_refresh_token_%d", "expires_in": 3600}]], refresh_counter),
354+
}
355+
end,
356+
["http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"] = function()
357+
return nil, "connection refused"
358+
end
359+
}
360+
361+
with_http_mock(custom_responses, function(temp_access_token)
362+
local gcpToken = temp_access_token(nil, {
363+
auth_method_order = "adc",
364+
oauth_token_url = custom_oauth_url
365+
})
366+
367+
-- First token
368+
assert.same(gcpToken.token, "test_refresh_token_1")
369+
assert.same(gcpToken.authMethod, "SA")
370+
assert.equals(1, refresh_counter)
371+
372+
-- Force refresh
373+
gcpToken.expireTime = ngx.now() - 100
374+
assert.is_true(gcpToken:needsRefresh())
375+
376+
-- Access token property to trigger refresh
377+
local refreshed_token = gcpToken.token
378+
assert.same(refreshed_token, "test_refresh_token_2")
379+
assert.equals(2, refresh_counter)
380+
assert.is_false(gcpToken:needsRefresh())
381+
end)
382+
end)
383+
384+
it("should use custom metadata_url during WI token refresh", function()
385+
local custom_metadata_url = "http://custom-metadata.internal/token"
386+
local refresh_counter = 0
387+
local custom_responses = {
388+
["https://www.googleapis.com/oauth2/v4/token"] = {
389+
status = 400,
390+
body = [[{"error": "invalid_grant"}]],
391+
},
392+
[custom_metadata_url] = function()
393+
refresh_counter = refresh_counter + 1
394+
return {
395+
status = 200,
396+
body = string.format([[{"access_token": "test_wi_refresh_token_%d", "expires_in": 3600}]], refresh_counter),
397+
}
398+
end
399+
}
400+
401+
with_http_mock(custom_responses, function(temp_access_token)
402+
local gcpToken = temp_access_token(nil, {
403+
auth_method_order = "legacy",
404+
metadata_url = custom_metadata_url
405+
})
406+
407+
-- First token
408+
assert.same(gcpToken.token, "test_wi_refresh_token_1")
409+
assert.same(gcpToken.authMethod, "WI")
410+
assert.equals(1, refresh_counter)
411+
412+
-- Force refresh
413+
gcpToken.expireTime = ngx.now() - 100
414+
assert.is_true(gcpToken:needsRefresh())
415+
416+
-- Access token property to trigger refresh
417+
local refreshed_token = gcpToken.token
418+
assert.same(refreshed_token, "test_wi_refresh_token_2")
419+
assert.equals(2, refresh_counter)
420+
assert.is_false(gcpToken:needsRefresh())
421+
end)
422+
end)
423+
424+
it("should maintain backward compatibility with default URLs when no custom URLs provided", function()
425+
-- Test that when no custom URLs are provided, default URLs are used
426+
local gcpToken = access_token(nil, { auth_method_order = "legacy" })
427+
428+
assert.same(gcpToken.token, "test_wi")
429+
assert.same(gcpToken.oauthTokenUrl, "https://www.googleapis.com/oauth2/v4/token")
430+
assert.same(gcpToken.metadataUrl, "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token")
431+
assert.is_number(gcpToken.expireTime)
432+
assert.is_false(gcpToken:needsRefresh())
433+
end)
434+
435+
it("should use default URL when custom URL is nil", function()
436+
local gcpToken = access_token(nil, {
437+
auth_method_order = "legacy",
438+
oauth_token_url = nil, -- explicitly set to nil
439+
metadata_url = nil -- explicitly set to nil
440+
})
441+
442+
assert.same(gcpToken.token, "test_wi")
443+
assert.same(gcpToken.oauthTokenUrl, "https://www.googleapis.com/oauth2/v4/token")
444+
assert.same(gcpToken.metadataUrl, "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token")
445+
end)
446+
447+
it("should use custom oauth_token_url in JWT aud field", function()
448+
local custom_oauth_url = "https://custom.googleapis.com/oauth2/v4/token"
449+
local captured_request_body = nil
450+
451+
-- We need to modify the HTTP mock to capture request details
452+
local original_http = package.loaded["resty.luasocket.http"]
453+
package.loaded["resty.luasocket.http"] = {
454+
new = function()
455+
return {
456+
close = function() return true end,
457+
request_uri = function(self, url, opts)
458+
if url == custom_oauth_url then
459+
captured_request_body = opts.body
460+
return {
461+
status = 200,
462+
body = [[{"access_token": "test_custom_aud_token", "expires_in": 3600}]],
463+
}
464+
else
465+
return nil, "connection refused"
466+
end
467+
end,
468+
}
469+
end,
470+
}
471+
472+
package.loaded["resty.gcp.request.credentials.accesstoken"] = nil
473+
local temp_access_token = require "resty.gcp.request.credentials.accesstoken"
474+
475+
local gcpToken = temp_access_token(nil, {
476+
auth_method_order = "adc",
477+
oauth_token_url = custom_oauth_url
478+
})
479+
480+
assert.same(gcpToken.token, "test_custom_aud_token")
481+
assert.is_not_nil(captured_request_body)
482+
483+
-- Verify the JWT contains the custom URL (it's in the assertion field of the request)
484+
local cjson = require("cjson.safe").new()
485+
local request_data = cjson.decode(captured_request_body)
486+
assert.is_not_nil(request_data)
487+
assert.is_not_nil(request_data.assertion)
488+
489+
-- Restore original state
490+
package.loaded["resty.luasocket.http"] = original_http
491+
package.loaded["resty.gcp.request.credentials.accesstoken"] = nil
492+
end)
493+
494+
end)
495+
246496
end)

src/resty/gcp/request/credentials/accesstoken.lua

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ local semaphore = require "ngx.semaphore"
55

66
local SEMAPHORE_TIMEOUT = 30 -- semaphore timeout in seconds
77
local EXPIRY_WINDOW = 15 -- expiry window in seconds
8+
local DEFAULT_OAUTH_TOKEN_URL = "https://www.googleapis.com/oauth2/v4/token"
9+
local DEFAULT_METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"
810

911
-- Executes a xpcall but returns hard-errors as Lua 'nil+err' result.
1012
-- Handles max of 10 return values.
@@ -18,7 +20,8 @@ local function safe_call(f, ...)
1820
return nil, result
1921
end
2022

21-
local function GetJwtToken(serviceAccount)
23+
local function GetJwtToken(serviceAccount, oauth_token_url)
24+
oauth_token_url = oauth_token_url or DEFAULT_OAUTH_TOKEN_URL
2225
local saDecode, err = cjson.decode(serviceAccount)
2326
if type(saDecode) ~= "table" then
2427
ngx.log(ngx.ERR, "[accesstoken] Invalid GCP_SERVICE_ACCOUNT, expect JSON: ", tostring(err))
@@ -34,7 +37,7 @@ local function GetJwtToken(serviceAccount)
3437
local payload = {
3538
iss = saDecode.client_email,
3639
sub = saDecode.client_email,
37-
aud = "https://www.googleapis.com/oauth2/v4/token",
40+
aud = oauth_token_url,
3841
iat = timeNow,
3942
exp = timeNow + 3600,
4043
scope = "https://www.googleapis.com/auth/cloud-platform"
@@ -51,16 +54,16 @@ local function GetJwtToken(serviceAccount)
5154
return jwt_token
5255
end
5356

54-
local function GetAccessTokenByJwt(jwtToken)
57+
local function GetAccessTokenByJwt(jwtToken, oauth_token_url)
58+
oauth_token_url = oauth_token_url or DEFAULT_OAUTH_TOKEN_URL
5559
local client = http.new()
56-
local auth_url = "https://www.googleapis.com/oauth2/v4/token"
5760
local params = {
5861
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer",
5962
assertion = jwtToken
6063
}
6164
local res, err =
6265
client:request_uri(
63-
auth_url,
66+
oauth_token_url,
6467
{
6568
method = "POST",
6669
body = cjson.encode(params),
@@ -78,7 +81,8 @@ local function GetAccessTokenByJwt(jwtToken)
7881
return accessToken
7982
end
8083

81-
local function GetAccessTokenBySA(serviceAccount)
84+
local function GetAccessTokenBySA(serviceAccount, oauth_token_url)
85+
oauth_token_url = oauth_token_url or DEFAULT_OAUTH_TOKEN_URL
8286
ngx.log(ngx.DEBUG, "[accesstoken] Using Environment Service Account to get Access Token")
8387

8488
if not serviceAccount then
@@ -88,22 +92,22 @@ local function GetAccessTokenBySA(serviceAccount)
8892
error("Couldn't find GCP_SERVICE_ACCOUNT env variable")
8993
return
9094
end
91-
local jwtToken = GetJwtToken(serviceAccount)
92-
local res = assert(GetAccessTokenByJwt(jwtToken))
95+
local jwtToken = GetJwtToken(serviceAccount, oauth_token_url)
96+
local res = assert(GetAccessTokenByJwt(jwtToken, oauth_token_url))
9397
if res.error then
9498
ngx.log(ngx.ERR, "[accesstoken] Unable to get access token: ", res.error_description)
9599
return
96100
end
97101
return res, "SA"
98102
end
99103

100-
local function GetAccessTokenByWI()
104+
local function GetAccessTokenByWI(metadata_url)
105+
metadata_url = metadata_url or DEFAULT_METADATA_URL
101106
ngx.log(ngx.DEBUG, "[accesstoken] Using Workload Identity to get Access Token")
102107
local client = http.new()
103-
local auth_url = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"
104108
local res, err =
105109
client:request_uri(
106-
auth_url,
110+
metadata_url,
107111
{
108112
headers = {
109113
["Metadata-Flavor"] = "Google"
@@ -144,6 +148,8 @@ function AccessToken:new(gcpServiceAccount, opts)
144148
setmetatable(self, AccessToken)
145149

146150
self.expireWindow = opts.expireWindow or EXPIRY_WINDOW
151+
self.oauthTokenUrl = opts.oauth_token_url or DEFAULT_OAUTH_TOKEN_URL
152+
self.metadataUrl = opts.metadata_url or DEFAULT_METADATA_URL
147153

148154
local auth_method_order = opts.auth_method_order or "legacy"
149155
gcpServiceAccount = gcpServiceAccount or os.getenv("GCP_SERVICE_ACCOUNT")
@@ -153,19 +159,19 @@ function AccessToken:new(gcpServiceAccount, opts)
153159
-- and add the ADC (Application Default Credentials) option.
154160
if auth_method_order == "legacy" then
155161
-- First try via Workload Identity and then via Service Account
156-
accessToken, authMethod = safe_call(GetAccessTokenByWI)
162+
accessToken, authMethod = safe_call(GetAccessTokenByWI, self.metadataUrl)
157163
if not accessToken then
158-
accessToken, authMethod = safe_call(GetAccessTokenBySA, gcpServiceAccount)
164+
accessToken, authMethod = safe_call(GetAccessTokenBySA, gcpServiceAccount, self.oauthTokenUrl)
159165
end
160166

161167
-- This simulates the official behavior of Application Default Credentials
162168
-- See https://cloud.google.com/docs/authentication/application-default-credentials#order
163169
-- for more details.
164170
-- The implementation is not exactly the same but a similar order of precedence is followed.
165171
elseif auth_method_order == "adc" then
166-
accessToken, authMethod = safe_call(GetAccessTokenBySA, gcpServiceAccount)
172+
accessToken, authMethod = safe_call(GetAccessTokenBySA, gcpServiceAccount, self.oauthTokenUrl)
167173
if not accessToken then
168-
accessToken, authMethod = safe_call(GetAccessTokenByWI)
174+
accessToken, authMethod = safe_call(GetAccessTokenByWI, self.metadataUrl)
169175
end
170176

171177
else
@@ -214,9 +220,9 @@ function AccessToken:refresh()
214220

215221
local accessToken, err
216222
if (self.authMethod == "SA") then
217-
accessToken, err = safe_call(GetAccessTokenBySA, self.gcpServiceAccount)
223+
accessToken, err = safe_call(GetAccessTokenBySA, self.gcpServiceAccount, self.oauthTokenUrl)
218224
elseif (self.authMethod == "WI") then
219-
accessToken, err = safe_call(GetAccessTokenByWI)
225+
accessToken, err = safe_call(GetAccessTokenByWI, self.metadataUrl)
220226
end
221227

222228
if (accessToken) then

0 commit comments

Comments
 (0)