4

Our web apps are currently in C# running on Windows and IIS. We rely heavily on the Windows authentication scheme that is included in this environment. With Windows authentication enabled we can detect the identity of the connected user and perform authorization on what screens and operation they are able to use.

If I set up a Phoenix web application will it be possible to detect the identity of the connected user based on their current Windows login? If not is there an easy to use replacement for the Windows authentication?

Matthew MacFarland
  • 2,413
  • 3
  • 26
  • 34

1 Answers1

2

I just did this over the weekend. Yes, it is possible. You have to use the HttpPlatformHandler add-on for IIS to make it work. HttpPlatformHandler has the forwardWindowsAuthToken configuration setting that you can use to forward the Windows user token for the authenticated user from IIS to your Phoenix application which is running as a child process. You have to use NIFs to process the token and get the Windows username or SID. As you'll note from the docs, you need to call CloseHandle to release the Windows user token for each request.

(I apologize in advance if the code below is not up to best practices. I'm new to Elixir and am actively trying to learn how to write better code. This was also from a hacking session trying to figure out the solution this weekend, so it's also not necessarily polished either.)

To do this, I packaged everything into a custom plug that I could put into the pipeline (I removed Logger statements to compress the size of the example):

defmodule MyApp.WindowsAuthentication do
  import Plug.Conn

  require Logger

  @on_load :load_nifs

  def load_nifs do
    if match? {:win32, _}, :os.type do
      :erlang.load_nif("./priv/windows_authentication", 0)
    else
      :ok
    end
  end

  def init(options), do: options

  def call(conn, _options) do
    if match? {:win32, _}, :os.type do
      case get_req_header(conn, "x-iis-windowsauthtoken") do
        [token_handle_string] ->
          # token_handle_string is a hex string
          token_handle = String.to_integer(token_handle_string, 16)
          case do_get_windows_username(token_handle) do
            {:ok, {domain_name, username}} ->
              conn = assign(conn, :windows_user, {domain_name, username})
            error ->
              Logger.error IO.inspect(error)
          end

          do_close_handle(token_handle)
        [] ->
          Logger.debug "X-IIS-WindowsAuthToken was not present"
      end
    end

    conn
  end

  def do_get_windows_username(_token_handle) do
    raise "do_get_windows_username/1 is only available on Windows"
  end

  def do_close_handle(_handle) do
    raise "do_close_handle/1 is only available on Windows"
  end
end

The C source code for the NIFs is below:

#include <Windows.h>
#include <erl_nif.h>

static const char* error_atom = "error";
static const char* invalid_token_handle_atom = "invalid_token_handle";
static const char* ok_atom = "ok";
static const char* win32_error_atom = "win32_error";

#define MAX_NAME 256

static HANDLE get_user_token(ErlNifEnv *env, ERL_NIF_TERM token) {
  HANDLE token_handle;

  if (!enif_get_ulong(env, token, (unsigned long *)&token_handle)) {
    return NULL;
  }

  return token_handle;
}

static ERL_NIF_TERM make_win32_error_tuple(ErlNifEnv* env, DWORD error_code) {
  return enif_make_tuple2(
    env,
    enif_make_atom(env, error_atom),
    enif_make_ulong(env, error_code)
  );
}

static ERL_NIF_TERM make_invalid_token_handle_error(ErlNifEnv* env) {
  return enif_make_tuple2(
    env,
    enif_make_atom(env, error_atom),
    enif_make_atom(env, invalid_token_handle_atom)
  );
}

static ERL_NIF_TERM do_get_windows_username(ErlNifEnv* env, int argc, ERL_NIF_TERM argv[]) {
  HANDLE token_handle;
  DWORD token_user_length;
  PTOKEN_USER token_user;
  DWORD last_error;
  WCHAR username[MAX_NAME];
  DWORD username_length = MAX_NAME;
  WCHAR domain_name[MAX_NAME];
  DWORD domain_name_length = MAX_NAME;
  size_t converted_chars;
  char converted_username[MAX_NAME * 2];
  char converted_domain_name[MAX_NAME * 2];
  errno_t err;
  BOOL succeeded;
  SID_NAME_USE sid_name_use;

  token_handle = get_user_token(env, argv[0]);
  if (!token_handle) {
    return make_invalid_token_handle_error(env);
  }

  if (!GetTokenInformation(token_handle, TokenUser, NULL, 0, &token_user_length)) {
    last_error = GetLastError();
    if (ERROR_INSUFFICIENT_BUFFER != last_error) {
      return make_win32_error_tuple(env, last_error);
    }
  }

  token_user = (PTOKEN_USER)malloc(token_user_length);
  if (!GetTokenInformation(token_handle, TokenUser, token_user, token_user_length, &token_user_length)) {
    free(token_user);
    return make_win32_error_tuple(env, GetLastError());
  }

  succeeded = LookupAccountSidW(
    NULL,
    token_user->User.Sid,
    username,
    &username_length,
    domain_name,
    &domain_name_length,
    &sid_name_use);
  if (!succeeded) {
    free(token_user);
    return make_win32_error_tuple(env, GetLastError());
  }

  err = wcstombs_s(&converted_chars, converted_username, 512, username, username_length);
  err = wcstombs_s(&converted_chars, converted_domain_name, 512, domain_name, domain_name_length);

  free(token_user);
  return enif_make_tuple2(
    env,
    enif_make_atom(env, ok_atom),
    enif_make_tuple2(
      env,
      enif_make_string(env, converted_domain_name, ERL_NIF_LATIN1),
      enif_make_string(env, converted_username, ERL_NIF_LATIN1)
    )
  );
}

static ERL_NIF_TERM do_close_handle(ErlNifEnv* env, int argc, ERL_NIF_TERM argv[]) {
  HANDLE token_handle;

  token_handle = get_user_token(env, argv[0]);
  if (!token_handle) {
    return make_invalid_token_handle_error(env);
  }

  if (!CloseHandle(token_handle)) {
    return make_win32_error_tuple(env, GetLastError());
  }

  return enif_make_atom(env, ok_atom);
}

static ErlNifFunc nif_functions[] = {
  { "do_close_handle", 1, do_close_handle },
  { "do_get_windows_username", 1, do_get_windows_username }
};

ERL_NIF_INIT(
  Elixir.MyApp.WindowsAuthentication,
  nif_functions,
  NULL,
  NULL,
  NULL,
  NULL
)

You can compile the C code using the 64-bit Visual Studio C++ tools (open the x64 VS command prompt). I tried this out with the new VS2017 tools. Put the DLL in the priv directory of your application.

cl /LD /I "C:\Program Files\erl-8.2\erts-8.2\include" /DDEBUG windows_authentication.c advapi32.lib

To run the plug, add it to your pipeline in web/router.ex:

pipeline :browser do
  plug :accepts, ["html"]
  plug MyApp.WindowsAuthentication
  plug :fetch_session
  plug :fetch_flash
  plug :protect_from_forgery
  plug :put_secure_browser_headers
end

The end result of this is that conn.assigns.windows_user will contain a tuple of the form {domain_name, username} that has the Windows domain username for the authenticated user.

Note: When I was trying this, I found CPU and memory leak issues from erl.exe when running as a child process of IIS. I'm still trying to figure that out in case you see it. I posted a question about it here.

I'll probably release this as a library on hex.pm when I've cleaned it up and fixed the memory/CPU issue, but for now, here's the code that will let you use Windows authentication with Phoenix.

Community
  • 1
  • 1
Michael Collins
  • 289
  • 2
  • 13