Robert's Data Science Blog

Authentication with Active Directory/Kerberos in Julia

When accessing REST APIs from Julia I prefer to use the HTTP package.

However, some of the APIs I use rely on authentication using the dark magic known as Active Directory and HTTP does not support this.

The {httr} package for R supports this by using the {curl} package with

httr::authenticate(":", ":", "gssnegotiate")

The HTTP package does not use cURL directly, but the LibCURL package offers an interface to libcurl.

I am no expert on libcurl/cURL, so this is based on a discourse post and a few tips on relevant cURL options. In order to make cURL write the request into Julia we need a callback function, that I copied directly from the Discourse post:

function curl_write_cb(curlbuf::Ptr{Cvoid}, s::Csize_t, n::Csize_t, p_ctxt::Ptr{Cvoid})::Csize_t
    sz = s * n
    data = Array{UInt8}(undef, sz)

    ccall(:memcpy, Ptr{Cvoid}, (Ptr{Cvoid}, Ptr{Cvoid}, UInt64), data, curlbuf, sz)

    if p_ctxt == C_NULL
        j_ctxt = nothing
    else
        j_ctxt = unsafe_pointer_to_objref(p_ctxt)
    end

    append!(j_ctxt, data)

    sz
end

There is an ancient blog post about callback functions in Julia. With this callback function we now want to access the URL <URL>. I comment most of the options, but they are collected in a function in my actual use case.

First the setup of the cURL call:

curl = LibCURL.curl_easy_init()

Then the callback:

c_curl_write_cb = @cfunction(curl_write_cb, Csize_t, (Ptr{Cvoid}, Csize_t, Csize_t, Ptr{Cvoid}))
LibCURL.curl_easy_setopt(curl, LibCURL.CURLOPT_WRITEFUNCTION, c_curl_write_cb)

The URL is set:

LibCURL.curl_easy_setopt(curl, LibCURL.CURLOPT_FOLLOWLOCATION, 1)
LibCURL.curl_easy_setopt(curl, LibCURL.CURLOPT_URL, "<URL>")

If it is relevant, we can set a header:

accept = "Accept: application/json"
header = LibCURL.curl_slist(pointer(accept), Ptr{Nothing}())
LibCURL.curl_easy_setopt(curl, LibCURL.CURLOPT_HTTPHEADER, header)

We now arrive at the Kerberos magic 🦄:

LibCURL.curl_easy_setopt(curl, LibCURL.CURLOPT_USERNAME, "")
LibCURL.curl_easy_setopt(curl, LibCURL.CURLOPT_USERNAME, "")
LibCURL.curl_easy_setopt(curl, LibCURL.CURLOPT_HTTPAUTH, LibCURL.CURLAUTH_GSSNEGOTIATE)

We construct the array that is going to hold the response:

buffer = UInt8[]
LibCURL.curl_easy_setopt(curl, LibCURL.CURLOPT_WRITEDATA, pointer_from_objref(buffer))

Finally, we make the call and inform Julia to preserve this array:

GC.@preserve buffer begin
    LibCURL.curl_easy_perform(curl)
    body = deepcopy(buffer)
end

I am not certain that it is necessary to copy buffer, but when using pointers I prefer to be careful.

Besides the actual body of the response we might be interested in the status code:

http_code = Array{Clong}(undef, 1)
LibCURL.curl_easy_getinfo(curl, LibCURL.CURLINFO_RESPONSE_CODE, http_code)
status = Int64(http_code[1])

When we are all done treating the response we ask cURL to clean up:

LibCURL.curl_easy_cleanup(curl)