Skip to content
Merged
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
22 changes: 19 additions & 3 deletions src/util.jl
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ function parse_www_authenticate(header::AbstractString)
idx = after_peek
continue
end
after_peek = skip_spaces(header, after_peek, stop)
if after_peek <= stop && header[after_peek] == '='
idx = peek_idx
else
Expand All @@ -196,14 +197,29 @@ function parse_www_authenticate(header::AbstractString)
end
end
key_start = idx
key, idx = read_token(header, idx, stop)
key, after_key = read_token(header, idx, stop)
isempty(key) && break
idx = skip_spaces(header, idx, stop)
idx = skip_spaces(header, after_key, stop)
if idx <= stop && header[idx] == '='
# A token68 can end in padding, while auth-param needs a value.
if !seen_param && token === nothing && idx == after_key
padding_end = idx
while padding_end <= stop && header[padding_end] == '='
padding_end = Base.nextind(header, padding_end)
end
next = skip_spaces(header, padding_end, stop)
if (next > stop || header[next] == ',') &&
all(c -> 'A' <= c <= 'Z' || 'a' <= c <= 'z' ||
'0' <= c <= '9' || c in ('-', '.', '_', '~', '+', '/'), key)
token = String(SubString(header, key_start, Base.prevind(header, padding_end)))
idx = next
break
end
end
idx = Base.nextind(header, idx)
idx = skip_spaces(header, idx, stop)
value, idx = read_value(header, idx, stop)
params[String(key)] = value
params[lowercase(key)] = value
seen_param = true
else
if seen_param || token !== nothing
Expand Down
46 changes: 45 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,50 @@ using OAuth
using Sockets
using ModelContextProtocol

@testset "Authentication parameter whitespace" begin
for whitespace in (" ", "\t", " \t "), quoted in (false, true)
metadata = "https://example.com/meta"
value = quoted ? string('"', metadata, '"') : metadata
header = "Bearer realm=\"api\", resource_metadata$(whitespace)=$(whitespace)$(value), scope$(whitespace)=\"openid profile\", Basic realm=\"other\""
challenges = ModelContextProtocol.extract_auth_challenges(
ModelContextProtocol.build_headers(["WWW-Authenticate" => header]))
@test length(challenges) == 2
@test challenges[1].challenge.scheme == "Bearer"
@test challenges[1].resource_metadata == metadata
@test challenges[1].scopes == ["openid", "profile"]
@test challenges[2].challenge.scheme == "Basic"
@test challenges[2].challenge.params["realm"] == "other"
end
end

@testset "Padded authentication challenge tokens" begin
for token in ("abc=", "abc==", "azAZ09-._~+/=="), whitespace in ("", " \t")
parsed = ModelContextProtocol.parse_www_authenticate("Negotiate $token$whitespace, Basic realm=\"backup\"")
@test length(parsed) == 2
@test parsed[1].scheme == "Negotiate"
@test parsed[1].token == token
@test isempty(parsed[1].params)
@test parsed[2].params["realm"] == "backup"
single = only(ModelContextProtocol.parse_www_authenticate("Negotiate $token$whitespace"))
@test single.token == token
@test isempty(single.params)
end
params = only(ModelContextProtocol.parse_www_authenticate("Bearer realm = \"\", error = invalid_token"))
@test params.token === nothing
@test params.params == Dict("realm" => "", "error" => "invalid_token")
end

@testset "Authentication parameter case" begin
challenges = ModelContextProtocol.extract_auth_challenges(
ModelContextProtocol.build_headers([
"WWW-Authenticate" => "Bearer RESOURCE_METADATA=\"https://example.test/Mixed\", SCOPE=\"OpenID Profile\", ReAlM=\"MiXeD\"",
]))
@test length(challenges) == 1
@test challenges[1].resource_metadata == "https://example.test/Mixed"
@test challenges[1].scopes == ["OpenID", "Profile"]
@test challenges[1].challenge.params["realm"] == "MiXeD"
end

mutable struct StubState
headers::Vector{Dict{String,String}}
cancellations::Vector{Dict{String,Any}}
Expand Down Expand Up @@ -378,7 +422,7 @@ function start_auth_stub_server()
HTTP.register!(router, "GET", "/.well-known/auth-required.json", req -> begin
host = HTTP.header(req, "Host")
base = string("http://", host)
header = "Bearer resource_metadata=\"$(base)/.well-known/protected-resource\" scope=\"openid profile\""
header = "Bearer realm=\"api\", resource_metadata = \"$(base)/.well-known/protected-resource\", scope = \"openid profile\""
HTTP.Response(401, ["WWW-Authenticate" => header], "")
end)
HTTP.register!(router, "GET", "/.well-known/protected-resource", req -> begin
Expand Down
Loading