Skip to content

Commit cb4b9b4

Browse files
committed
Merge branch 'pr-158-take-2'
2 parents e511331 + 13516c4 commit cb4b9b4

File tree

3 files changed

+135
-31
lines changed

3 files changed

+135
-31
lines changed

lib/resty/openidc.lua

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,51 @@ local function openidc_load_jwt_and_verify_crypto(opts, jwt_string, asymmetric_s
830830
return jwt_obj
831831
end
832832

833+
--
834+
-- Load and validate id token from the id_token properties of the token endpoint response
835+
-- Parameters :
836+
-- - opts the openidc module options
837+
-- - jwt_id_token the id_token from the id_token properties of the token endpoint response
838+
-- - session the current session
839+
-- Return the id_token, nil if valid
840+
-- Return nil, the error if invalid
841+
--
842+
local function openidc_load_and_validate_jwt_id_token(opts, jwt_id_token, session)
843+
844+
local jwt_obj, err = openidc_load_jwt_and_verify_crypto(opts, jwt_id_token, opts.secret, opts.client_secret,
845+
opts.discovery.id_token_signing_alg_values_supported)
846+
if err then
847+
local alg = (jwt_obj and jwt_obj.header and jwt_obj.header.alg) or ''
848+
local is_unsupported_signature_error = jwt_obj and not jwt_obj.verified and not is_algorithm_supported(jwt_obj.header)
849+
if is_unsupported_signature_error then
850+
if opts.accept_unsupported_alg == nil or opts.accept_unsupported_alg then
851+
ngx.log(ngx.WARN, "ignored id_token signature as algorithm '" .. alg .. "' is not supported")
852+
else
853+
err = "token is signed using algorithm \"" .. alg .. "\" which is not supported by lua-resty-jwt"
854+
ngx.log(ngx.ERR, err)
855+
return nil, err
856+
end
857+
else
858+
ngx.log(ngx.ERR, "id_token '" .. alg .. "' signature verification failed")
859+
return nil, err
860+
end
861+
end
862+
local id_token = jwt_obj.payload
863+
864+
ngx.log(ngx.DEBUG, "id_token header: ", cjson.encode(jwt_obj.header))
865+
ngx.log(ngx.DEBUG, "id_token payload: ", cjson.encode(jwt_obj.payload))
866+
867+
-- validate the id_token contents
868+
if openidc_validate_id_token(opts, id_token, session.data.nonce) == false then
869+
err = "id_token validation failed"
870+
ngx.log(ngx.ERR, err)
871+
return nil, err
872+
end
873+
874+
return id_token
875+
876+
end
877+
833878
-- handle a "code" authorization response from the OP
834879
local function openidc_authorization_response(opts, session)
835880
local args = ngx.req.get_uri_args()
@@ -880,34 +925,8 @@ local function openidc_authorization_response(opts, session)
880925
return nil, err, session.data.original_url, session
881926
end
882927

883-
local jwt_obj
884-
jwt_obj, err = openidc_load_jwt_and_verify_crypto(opts, json.id_token, opts.secret, opts.client_secret,
885-
opts.discovery.id_token_signing_alg_values_supported)
928+
local id_token, err = openidc_load_and_validate_jwt_id_token(opts, json.id_token, session);
886929
if err then
887-
local alg = (jwt_obj and jwt_obj.header and jwt_obj.header.alg) or ''
888-
local is_unsupported_signature_error = jwt_obj and not jwt_obj.verified and not is_algorithm_supported(jwt_obj.header)
889-
if is_unsupported_signature_error then
890-
if opts.accept_unsupported_alg == nil or opts.accept_unsupported_alg then
891-
ngx.log(ngx.WARN, "ignored id_token signature as algorithm '" .. alg .. "' is not supported")
892-
else
893-
err = "token is signed using algorithm \"" .. alg .. "\" which is not supported by lua-resty-jwt"
894-
ngx.log(ngx.ERR, err)
895-
return nil, err, session.data.original_url, session
896-
end
897-
else
898-
ngx.log(ngx.ERR, "id_token '" .. alg .. "' signature verification failed")
899-
return nil, err, session.data.original_url, session
900-
end
901-
end
902-
local id_token = jwt_obj.payload
903-
904-
ngx.log(ngx.DEBUG, "id_token header: ", cjson.encode(jwt_obj.header))
905-
ngx.log(ngx.DEBUG, "id_token payload: ", cjson.encode(jwt_obj.payload))
906-
907-
-- validate the id_token contents
908-
if openidc_validate_id_token(opts, id_token, session.data.nonce) == false then
909-
err = "id_token validation failed"
910-
ngx.log(ngx.ERR, err)
911930
return nil, err, session.data.original_url, session
912931
end
913932

@@ -1090,16 +1109,35 @@ local function openidc_access_token(opts, session, try_to_renew)
10901109
if err then
10911110
return nil, err
10921111
end
1112+
local id_token
1113+
if json.id_token then
1114+
id_token, err = openidc_load_and_validate_jwt_id_token(opts, json.id_token, session)
1115+
if err then
1116+
ngx.log(ngx.ERR, "invalid id token, discarding tokens returned while refreshing")
1117+
return nil, err
1118+
end
1119+
end
10931120
ngx.log(ngx.DEBUG, "access_token refreshed: ", json.access_token, " updated refresh_token: ", json.refresh_token)
10941121

10951122
session:start()
10961123
session.data.access_token = json.access_token
10971124
session.data.access_token_expiration = current_time + openidc_access_token_expires_in(opts, json.expires_in)
1098-
if json.refresh_token ~= nil then
1125+
if json.refresh_token then
10991126
session.data.refresh_token = json.refresh_token
11001127
end
11011128

1102-
-- save the session with the new access_token and optionally the new refresh_token
1129+
if json.id_token and
1130+
(store_in_session(opts, 'enc_id_token') or store_in_session(opts, 'id_token')) then
1131+
ngx.log(ngx.DEBUG, "id_token refreshed: ", json.id_token)
1132+
if store_in_session(opts, 'enc_id_token') then
1133+
session.data.enc_id_token = json.id_token
1134+
end
1135+
if store_in_session(opts, 'id_token') then
1136+
session.data.id_token = id_token
1137+
end
1138+
end
1139+
1140+
-- save the session with the new access_token and optionally the new refresh_token and id_token
11031141
session:save()
11041142

11051143
return session.data.access_token, err

tests/spec/test_support.lua

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ local DEFAULT_FAKE_ACCESS_TOKEN_SIGNATURE = "false"
8383
local DEFAULT_FAKE_ID_TOKEN_SIGNATURE = "false"
8484
local DEFAULT_BREAK_ID_TOKEN_SIGNATURE = "false"
8585
local DEFAULT_NONE_ALG_ID_TOKEN_SIGNATURE = "false"
86+
local DEFAULT_REFRESH_RESPONSE_CONTAINS_ID_TOKEN = "true"
8687

8788
local DEFAULT_UNAUTH_ACTION = "nil"
8889

@@ -192,8 +193,13 @@ JWT_VERIFY_SECRET]=]
192193
local auth = ngx.req.get_headers()["Authorization"]
193194
ngx.log(ngx.ERR, "token authorization header: " .. (auth and auth or ""))
194195
ngx.header.content_type = 'application/json;charset=UTF-8'
195-
local id_token = ID_TOKEN
196196
local args = ngx.req.get_post_args()
197+
local id_token
198+
if args.grant_type == "authorization_code" then
199+
id_token = ID_TOKEN
200+
else
201+
id_token = REFRESH_ID_TOKEN
202+
end
197203
local access_token = "a_token"
198204
local refresh_token = "r_token"
199205
if args.grant_type == "authorization_code" then
@@ -226,8 +232,10 @@ JWT_VERIFY_SECRET]=]
226232
access_token = access_token,
227233
expires_in = TOKEN_RESPONSE_EXPIRES_IN,
228234
refresh_token = TOKEN_RESPONSE_CONTAINS_REFRESH_TOKEN and refresh_token or nil,
229-
id_token = jwt_token
230235
}
236+
if args.grant_type == "authorization_code" or REFRESH_RESPONSE_CONTAINS_ID_TOKEN then
237+
token_response.id_token = jwt_token
238+
end
231239
delay(TOKEN_DELAY_RESPONSE)
232240
ngx.say(cjson.encode(token_response))
233241
}
@@ -348,6 +356,7 @@ local function write_config(out, custom_config)
348356
custom_config = custom_config or {}
349357
local oidc_config = merge(merge({}, DEFAULT_OIDC_CONFIG), custom_config["oidc_opts"] or {})
350358
local id_token = merge(merge({}, DEFAULT_ID_TOKEN), custom_config["id_token"] or {})
359+
local refresh_id_token = merge({}, id_token)
351360
local verify_opts = merge(merge({}, DEFAULT_VERIFY_OPTS), custom_config["verify_opts"] or {})
352361
local access_token = merge(merge({}, DEFAULT_ACCESS_TOKEN), custom_config["access_token"] or {})
353362
local token_header = merge(merge({}, DEFAULT_TOKEN_HEADER), custom_config["token_header"] or {})
@@ -360,10 +369,14 @@ local function write_config(out, custom_config)
360369
local token_response_contains_refresh_token = custom_config["token_response_contains_refresh_token"]
361370
or DEFAULT_TOKEN_RESPONSE_CONTAINS_REFRESH_TOKEN
362371
local refreshing_token_fails = custom_config["refreshing_token_fails"] or DEFAULT_REFRESHING_TOKEN_FAILS
372+
local refresh_response_contains_id_token = custom_config["refresh_response_contains_id_token"] or DEFAULT_REFRESH_RESPONSE_CONTAINS_ID_TOKEN
363373
local access_token_opts = merge(merge({}, DEFAULT_OIDC_CONFIG), custom_config["access_token_opts"] or {})
364374
for _, k in ipairs(custom_config["remove_id_token_claims"] or {}) do
365375
id_token[k] = nil
366376
end
377+
for _, k in ipairs(custom_config["remove_refresh_id_token_claims"] or {}) do
378+
refresh_id_token[k] = nil
379+
end
367380
for _, k in ipairs(custom_config["remove_access_token_claims"] or {}) do
368381
access_token[k] = nil
369382
end
@@ -383,6 +396,7 @@ local function write_config(out, custom_config)
383396
:gsub("TOKEN_RESPONSE_EXPIRES_IN", token_response_expires_in)
384397
:gsub("TOKEN_RESPONSE_CONTAINS_REFRESH_TOKEN", token_response_contains_refresh_token)
385398
:gsub("REFRESHING_TOKEN_FAILS", refreshing_token_fails)
399+
:gsub("REFRESH_RESPONSE_CONTAINS_ID_TOKEN", refresh_response_contains_id_token)
386400
:gsub("ACCESS_TOKEN_OPTS", serpent.block(access_token_opts, {comment = false }))
387401
:gsub("JWK_DELAY_RESPONSE", ((custom_config["delay_response"] or {}).jwk or DEFAULT_DELAY_RESPONSE))
388402
:gsub("TOKEN_DELAY_RESPONSE", ((custom_config["delay_response"] or {}).token or DEFAULT_DELAY_RESPONSE))
@@ -395,6 +409,7 @@ local function write_config(out, custom_config)
395409
:gsub("FAKE_ID_TOKEN_SIGNATURE", custom_config["fake_id_token_signature"] or DEFAULT_FAKE_ID_TOKEN_SIGNATURE)
396410
:gsub("BREAK_ID_TOKEN_SIGNATURE", custom_config["break_id_token_signature"] or DEFAULT_BREAK_ID_TOKEN_SIGNATURE)
397411
:gsub("NONE_ALG_ID_TOKEN_SIGNATURE", custom_config["none_alg_id_token_signature"] or DEFAULT_NONE_ALG_ID_TOKEN_SIGNATURE)
412+
:gsub("REFRESH_ID_TOKEN", serpent.block(refresh_id_token, {comment = false }))
398413
:gsub("ID_TOKEN", serpent.block(id_token, {comment = false }))
399414
:gsub("ACCESS_TOKEN", serpent.block(access_token, {comment = false }))
400415
:gsub("UNAUTH_ACTION", custom_config["unauth_action"] and ('"' .. custom_config["unauth_action"] .. '"') or DEFAULT_UNAUTH_ACTION)

tests/spec/token_refresh_spec.lua

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ describe("if there is an active but expired login and refresh is not configured
5959
it("the token gets refreshed", function()
6060
assert.error_log_contains("request body for token endpoint call: .*grant_type=refresh_token.*")
6161
end)
62+
-- token endpoint response contains id token by default
63+
it ("the id token gets refreshed", function()
64+
assert.error_log_contains("id_token refreshed")
65+
end)
6266
it("the access token is returned by authenticate", function()
6367
assert.is_not.error_log_contains("authenticate didn't return any access token")
6468
end)
@@ -153,3 +157,50 @@ describe("if there is an active but expired login and refreshing it fails", func
153157
end)
154158
end)
155159

160+
describe("if token refresh doesn't add a new id_token", function()
161+
test_support.start_server({
162+
token_response_expires_in = 0,
163+
refresh_response_contains_id_token = "false",
164+
})
165+
teardown(test_support.stop_server)
166+
local _, _, cookies = test_support.login()
167+
os.execute("sleep 1.5")
168+
local _, status = http.request({
169+
url = "http://localhost/default/t",
170+
redirect = false,
171+
headers = { cookie = cookies },
172+
})
173+
it("no redirect occurs on the next call", function()
174+
assert.are.equals(200, status)
175+
end)
176+
it ("the id token doesn't get refreshed", function()
177+
assert.is_not.error_log_contains("id_token refreshed")
178+
end)
179+
it("the access token is returned by authenticate", function()
180+
assert.is_not.error_log_contains("authenticate didn't return any access token")
181+
end)
182+
end)
183+
184+
describe("if refresh contains an invalid id_token", function()
185+
test_support.start_server({
186+
token_response_expires_in = 0,
187+
remove_refresh_id_token_claims = { "iss" }
188+
})
189+
teardown(test_support.stop_server)
190+
local _, _, cookies = test_support.login()
191+
os.execute("sleep 1.5")
192+
local _, status = http.request({
193+
url = "http://localhost/default/t",
194+
redirect = false,
195+
headers = { cookie = cookies },
196+
})
197+
it ("the id token doesn't get refreshed", function()
198+
assert.is_not.error_log_contains("id_token refreshed")
199+
end)
200+
it("the tokens are rejected", function()
201+
assert.error_log_contains("invalid id token, discarding tokens returned while refreshing")
202+
end)
203+
it("the access token is discarded", function()
204+
assert.error_log_contains("lost access token")
205+
end)
206+
end)

0 commit comments

Comments
 (0)