"""
Return whether the `request` was authenticated in one of two ways:
1. the session's `secret` was included in the URL as a search parameter, or
2. the session's `secret` was included in a cookie.
"""
function is_authenticated(session::ServerSession, request::HTTP.Request)
(
secret_in_url = try
uri = HTTP.URI(request.target)
query = HTTP.queryparams(uri)
get(query, "secret", "") == session.secret
catch e
@warn "Failed to authenticate request using URL" exception = (e, catch_backtrace())
false
end
) || (
secret_in_cookie = try
cookies = HTTP.cookies(request)
any(cookies) do cookie
cookie.name == "secret" && cookie.value == session.secret
end
catch e
@warn "Failed to authenticate request using cookies" exception = (e, catch_backtrace())
false
end
)
# that ) || ( kind of looks like Krabs from spongebob
end
# Function to log the url with secret on the Julia CLI when a request comes to the server without the secret. Executes at most once every 5 seconds
const log_secret_throttled = simple_leading_throttle(5) do session::ServerSession, request::HTTP.Request
host = HTTP.header(request, "Host")
target = request.target
url = Text(string(HTTP.URI(HTTP.URI("http://$host/"); query=Dict("secret" => session.secret))))
@info("No longer authenticated? Visit this URL to continue:", url)
end
function add_set_secret_cookie!(session::ServerSession, response::HTTP.Response)
HTTP.setheader(response, "Set-Cookie" => "secret=$(session.secret); SameSite=Strict; HttpOnly")
response
end
# too many layers i know
"""
Generate a middleware (i.e. a function `HTTP.Handler -> HTTP.Handler`) that stores the `session` in every `request`'s context.
"""
function create_session_context_middleware(session::ServerSession)
function session_context_middleware(handler::Function)::Function
function(request::HTTP.Request)
request.context[:pluto_session] = session
handler(request)
end
end
end
session_from_context(request::HTTP.Request) = request.context[:pluto_session]::ServerSession
function auth_required(session::ServerSession, request::HTTP.Request)
path = HTTP.URI(request.target).path
ext = splitext(path)[2]
security = session.options.security
if path ∈ ("/ping", "/possible_binder_token_please") || ext ∈ (".ico", ".js", ".css", ".png", ".gif", ".svg", ".ico", ".woff2", ".woff", ".ttf", ".eot", ".otf", ".json", ".map")
false
elseif path ∈ ("", "/")
# / does not need security.require_secret_for_open_links, because this is how we handle the configuration where:
# require_secret_for_open_links == true
# require_secret_for_access == false
#
# This means that access to all 'risky' endpoints is restricted to authenticated requests (to prevent CSRF), but we allow an unauthenticated request to visit the `/` page and acquire the cookie (see `add_set_secret_cookie!`).
#
# (By default, `require_secret_for_access` (and `require_secret_for_open_links`) is `true`.)
security.require_secret_for_access
else
security.require_secret_for_access ||
security.require_secret_for_open_links
end
end
"""
auth_middleware(f::HTTP.Handler) -> HTTP.Handler
Returns an `HTTP.Handler` (i.e. a function `HTTP.Request → HTTP.Response`) which does three things:
1. Check whether the request is authenticated (by calling `is_authenticated`), if not, return a 403 error.
2. Call your `f(request)` to create the response message.
3. Add a `Set-Cookie` header to the response with the session's `secret`.
This is for HTTP requests, the authentication mechanism for WebSockets is separate.
"""
function auth_middleware(handler)
return function (request::HTTP.Request)
session = session_from_context(request)
required = auth_required(session, request)
if !required || is_authenticated(session, request)
response = handler(request)
if !required
filter!(p -> p[1] != "Access-Control-Allow-Origin", response.headers)
HTTP.setheader(response, "Access-Control-Allow-Origin" => "*")
end
if required || HTTP.URI(request.target).path ∈ ("", "/")
add_set_secret_cookie!(session, response)
end
response
else
log_secret_throttled(session, request)
error_response(403, "Not yet authenticated", "Open the link that was printed in the terminal where you launched Pluto. It includes a secret, which is needed to access this server.
If you are running the server yourself and want to change this configuration, have a look at the keyword arguments to Pluto.run.
Please report this error if you did not expect it!")
end
end
end