superstator
superstator

Reputation: 3218

Parse WwwAuthenticate challenge string

I am working on a client for a RESTful service, using .NET Core 2.0. The remote service returns challenges like this:

WwwAuthenticate: Bearer realm="https://somesite/auth",service="some site",scope="some scope"

Which need to get turned into token requests like:

GET https://somesite/auth?service=some%20site&scope=some%20scope

Parsing the header to get a scheme and parameter is easy with AuthenticationHeaderValue, but that just gets me the realm="https://somesite/auth",service="some site",scope="some scope" string. How can I easily and reliably parse this to the individual realm, service, and scope components? It's not quite JSON, so deserializing it with NewtonSoft JsonConvert won't work. I could regex it into something that looks like XML or JSON, but that seems incredibly hacky (not to mention unreliable).

Surely there's a better way?

Upvotes: 1

Views: 535

Answers (2)

erdomke
erdomke

Reputation: 5245

Possible duplicate of How to parse values from Www-Authenticate

Using the schema defined in RFC6750 and RFC2616, a slightly more precise parser implementation is included below. This parser takes into account the possibility that strings might contain =, ,, and/or escaped ".

internal class AuthParamParser
{
  private string _buffer;
  private int _i;

  private AuthParamParser(string param)
  {
    _buffer = param;
    _i = 0;
  }

  public static Dictionary<string, string> Parse(string param)
  {
    var state = new AuthParamParser(param);
    var result = new Dictionary<string, string>();
    var token = state.ReadToken();
    while (!string.IsNullOrEmpty(token))
    {
      if (!state.ReadDelim('='))
        return result;
      result.Add(token, state.ReadString());
      if (!state.ReadDelim(','))
        return result;
      token = state.ReadToken();
    }
    return result;
  }

  private string ReadToken()
  {
    var start = _i;
    while (_i < _buffer.Length && ValidTokenChar(_buffer[_i]))
      _i++;
    return _buffer.Substring(start, _i - start);
  }

  private bool ReadDelim(char ch)
  {
    while (_i < _buffer.Length && char.IsWhiteSpace(_buffer[_i]))
      _i++;
    if (_i >= _buffer.Length || _buffer[_i] != ch)
      return false;
    _i++;
    while (_i < _buffer.Length && char.IsWhiteSpace(_buffer[_i]))
      _i++;
    return true;
  }

  private string ReadString()
  {
    if (_i < _buffer.Length && _buffer[_i] == '"')
    {
      var buffer = new StringBuilder();
      _i++;
      while (_i < _buffer.Length)
      {
        if (_buffer[_i] == '\\' && (_i + 1) < _buffer.Length)
        {
          _i++;
          buffer.Append(_buffer[_i]);
          _i++;
        }
        else if (_buffer[_i] == '"')
        {
          _i++;
          return buffer.ToString();
        }
        else
        {
          buffer.Append(_buffer[_i]);
          _i++;
        }
      }
      return buffer.ToString();
    }
    else
    {
      return ReadToken();
    }
  }

  private bool ValidTokenChar(char ch)
  {
    if (ch < 32)
      return false;
    if (ch == '(' || ch == ')' || ch == '<' || ch == '>' || ch == '@'
      || ch == ',' || ch == ';' || ch == ':' || ch == '\\' || ch == '"'
      || ch == '/' || ch == '[' || ch == ']' || ch == '?' || ch == '='
      || ch == '{' || ch == '}' || ch == 127 || ch == ' ' || ch == '\t')
      return false;
    return true;
  }
}

Upvotes: 1

Eser
Eser

Reputation: 12546

Since I don't see a non-hacky way. Maybe this hacky way may help

string input = @"WwwAuthenticate: Bearer realm=""https://somesite/auth"",service=""some site"",scope=""some, scope""";
var dict = Regex.Matches(input, @"[\W]+(\w+)=""(.+?)""").Cast<Match>()
          .ToDictionary(x => x.Groups[1].Value, x => x.Groups[2].Value);

var url = dict["realm"] + "?" + string.Join("&", dict.Where(x => x.Key != "realm").Select(x => x.Key + "=" + WebUtility.UrlEncode(x.Value)));

OUTPUT

url => https://somesite/auth?service=some+site&scope=some%2C+scope

BTW: I added a , in "scope"

Upvotes: 2

Related Questions