Skip to content
Open
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
57 changes: 52 additions & 5 deletions src/Owin.StatelessAuth.Tests/StatelessAuthTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
namespace Owin.StatelessAuth.Tests
{
using FakeItEasy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using Xunit;
using Xunit.Extensions;

Expand Down Expand Up @@ -215,11 +215,10 @@ public void Should_Add_User_To_Owin_Environment()
[Fact]
public void Should_Override_User_In_Owin_Environment()
{

//Given
var fakeTokenValidator = A.Fake<ITokenValidator>();

var secureuser = new ClaimsPrincipal();
var secureuser = new ClaimsPrincipal();
var claimsIdentity = new ClaimsIdentity("Token");
claimsIdentity.AddClaim(new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "DumbUser"));
secureuser.AddIdentity(claimsIdentity);
Expand Down Expand Up @@ -263,7 +262,7 @@ public void Should_Return_WWW_Authenticate_Header_In_Options()
};

var owinhttps = GetStatelessAuth(GetNextFunc(), statelessAuthOptions: options);

var environment = new Dictionary<string, object>
{
{ "owin.RequestHeaders", new Dictionary<string, string[]>() },
Expand All @@ -279,6 +278,49 @@ public void Should_Return_WWW_Authenticate_Header_In_Options()
Assert.Equal("Basic realm=\"WallyWorld\"", responseHeaders["WWW-Authenticate"].First());
}

[Fact]
public void Should_Execute_Next_If_Validated_Token_In_QueryString()
{
//Given
var owinhttps = GetStatelessAuth(GetNextFunc(), null, GetStatelessAuthOptionsQueryString());
var environment = new Dictionary<string, object>
{
{ "owin.RequestHeaders", new Dictionary<string, string[]>() },
{ "owin.RequestQueryString", "clientProtocol=1.5&Authorization=Token+eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJodHRwOi8vdHVlcmNvLmNvbSIsIkF1ZGllbmNlIjoiaHR0cDovL3RhbnRhbi5seSIsIkNsYWltcyI6W3siSXNzdWVyIjoiTE9DQUwgQVVUSE9SSVRZIiwiT3JpZ2luYWxJc3N1ZXIiOiJMT0NBTCBBVVRIT1JJVFkiLCJQcm9wZXJ0aWVzIjp7fSwiU3ViamVjdCI6bnVsbCwiVHlwZSI6Imh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL2VtYWlsYWRkcmVzcyIsIlZhbHVlIjoiaG9sYUBob2xhLmNvbSIsIlZhbHVlVHlwZSI6Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hI3N0cmluZyJ9LHsiSXNzdWVyIjoiTE9DQUwgQVVUSE9SSVRZIiwiT3JpZ2luYWxJc3N1ZXIiOiJMT0NBTCBBVVRIT1JJVFkiLCJQcm9wZXJ0aWVzIjp7fSwiU3ViamVjdCI6bnVsbCwiVHlwZSI6Imh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWVpZGVudGlmaWVyIiwiVmFsdWUiOiJob2xhIiwiVmFsdWVUeXBlIjoiaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEjc3RyaW5nIn0seyJJc3N1ZXIiOiJMT0NBTCBBVVRIT1JJVFkiLCJPcmlnaW5hbElzc3VlciI6IkxPQ0FMIEFVVEhPUklUWSIsIlByb3BlcnRpZXMiOnt9LCJTdWJqZWN0IjpudWxsLCJUeXBlIjoiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9wcmltYXJ5c2lkIiwiVmFsdWUiOiJVc2VyLzE4Njc5MDY2MjM3NzkiLCJWYWx1ZVR5cGUiOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSNzdHJpbmcifSx7Iklzc3VlciI6IkxPQ0FMIEFVVEhPUklUWSIsIk9yaWdpbmFsSXNzdWVyIjoiTE9DQUwgQVVUSE9SSVRZIiwiUHJvcGVydGllcyI6e30sIlN1YmplY3QiOm51bGwsIlR5cGUiOiJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiLCJWYWx1ZSI6IlBybyIsIlZhbHVlVHlwZSI6Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hI3N0cmluZyJ9XSwiRXhwaXJ5IjoiXC9EYXRlKDE0MzE0NjI4NzQ3MDcpXC8ifQ.eTTyY8fQKVgssmo42Gz82Ns742zD5uGo3PAjT_xEGMw&connectionData=%5B%7B%22name%22%3A%22superhub%22%7D%5D&_=1431419951254" },
{ "owin.RequestPath", "/" }
};

//When
var task = owinhttps.Invoke(environment);

//Then
Assert.Equal(true, task.IsCompleted);
Assert.Equal(123, ((Task<int>)task).Result);
}

[Theory]
[InlineData("/api/user")]
[InlineData("/api/user/js/main.css")]
public void Should_Return_401_If_Request_Path_Doesnt_Meet_Ignore_List_And_Empty_Auth_Header_And_Not_QueryString(string requestpath)
{
//Given
var owinhttps = GetStatelessAuth(GetNextFunc(), statelessAuthOptions: new StatelessAuthOptions() { IgnorePaths = new List<string>() { "/api/user/js/*.js", "/api/products" }, VerifyAuthenticationQueryString = true });
var environment = new Dictionary<string, object>
{
{ "owin.RequestHeaders", new Dictionary<string, string[]>() { { "Authorization", new[] { "" } } } }, //empty header so it falls through ignorelist check
{ "owin.RequestQueryString", "" },
{ "owin.RequestPath", requestpath }
};

//When
var task = owinhttps.Invoke(environment);

//Then
Assert.Equal(401, environment["owin.ResponseStatusCode"]);
Assert.Equal(true, task.IsCompleted);
Assert.Equal(0, ((Task<int>)task).Result);
}

public Func<IDictionary<string, object>, Task> GetNextFunc()
{
return objects => Task.FromResult(123);
Expand All @@ -296,6 +338,11 @@ private StatelessAuthOptions GetStatelessAuthOptions()
return new StatelessAuthOptions() { IgnorePaths = Enumerable.Empty<string>(), WWWAuthenticateChallenge = "Digest" };
}

private StatelessAuthOptions GetStatelessAuthOptionsQueryString()
{
return new StatelessAuthOptions() { IgnorePaths = Enumerable.Empty<string>(), WWWAuthenticateChallenge = "Digest", VerifyAuthenticationQueryString = true };
}

private ITokenValidator GetFakeTokenValidator()
{
var fakeTokenValidator = A.Fake<ITokenValidator>();
Expand All @@ -306,4 +353,4 @@ private ITokenValidator GetFakeTokenValidator()
return fakeTokenValidator;
}
}
}
}
4 changes: 3 additions & 1 deletion src/Owin.StatelessAuth/Owin.StatelessAuth.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Minimatch">
<Reference Include="Minimatch, Version=1.1.0.0, Culture=neutral, PublicKeyToken=0cadeb0b849c27c0, processorArchitecture=MSIL">
<HintPath>..\packages\Minimatch.1.1.0.0\lib\portable-net40+sl50+win+wp80\Minimatch.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Owin">
<HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath>
Expand All @@ -48,6 +49,7 @@
<Compile Include="StatelessAuth.cs" />
<Compile Include="StatelessAuthExtensions.cs" />
<Compile Include="StatelessAuthOptions.cs" />
<Compile Include="UrlEncodingParser.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
Expand Down
37 changes: 29 additions & 8 deletions src/Owin.StatelessAuth/StatelessAuth.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
namespace Owin.StatelessAuth
{
using Minimatch;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Minimatch;

public class StatelessAuth
{
Expand Down Expand Up @@ -35,7 +35,7 @@ public Task Invoke(IDictionary<string, object> environment)
{
foreach (var ignorePath in statelessAuthOptions.IgnorePaths)
{
var mm = new Minimatcher(ignorePath, new Options(){IgnoreCase = true});
var mm = new Minimatcher(ignorePath, new Options() { IgnoreCase = true });

if (mm.IsMatch(path))
{
Expand All @@ -44,14 +44,14 @@ public Task Invoke(IDictionary<string, object> environment)
}
}

var requestHeaders = (IDictionary<string, string[]>)environment["owin.RequestHeaders"];
if (!requestHeaders.ContainsKey("Authorization"))
string token = VerifyAuthorizationHeader(environment);

if (token == null && statelessAuthOptions.VerifyAuthenticationQueryString)
{
return AuthChallengeResponse(environment);
token = VerifyAuthorizationQueryString(environment);
}

var token = requestHeaders["Authorization"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(token))
if (token == null)
{
return AuthChallengeResponse(environment);
}
Expand All @@ -75,6 +75,27 @@ public Task Invoke(IDictionary<string, object> environment)
return nextFunc(environment);
}

private string VerifyAuthorizationHeader(IDictionary<string, object> environment)
{
string token = null;
var requestHeaders = (IDictionary<string, string[]>)environment["owin.RequestHeaders"];
if (requestHeaders.ContainsKey("Authorization"))
{
token = requestHeaders["Authorization"].FirstOrDefault();
token = (string.IsNullOrEmpty(token) ? null : token);
}

return token;
}

private string VerifyAuthorizationQueryString(IDictionary<string, object> environment)
{
var queryString = environment["owin.RequestQueryString"] as string;
var query = new UrlEncodingParser(queryString, statelessAuthOptions.DecodePlusSignsAsSpacesQueryString);
var token = query["Authorization"];
return (string.IsNullOrEmpty(token) ? null : token);
}

private Task AuthChallengeResponse(IDictionary<string, object> environment)
{
if (statelessAuthOptions != null && statelessAuthOptions.PassThroughUnauthorizedRequests)
Expand All @@ -100,4 +121,4 @@ private Task AuthChallengeResponse(IDictionary<string, object> environment)
return Task.FromResult(0);
}
}
}
}
8 changes: 7 additions & 1 deletion src/Owin.StatelessAuth/StatelessAuthOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
public class StatelessAuthOptions
{
public IEnumerable<string> IgnorePaths { get; set; }

public string WWWAuthenticateChallenge { get; set; }

public bool PassThroughUnauthorizedRequests { get; set; }

public bool VerifyAuthenticationQueryString { get; set; }

public bool DecodePlusSignsAsSpacesQueryString { get; set; }
}
}
}
145 changes: 145 additions & 0 deletions src/Owin.StatelessAuth/UrlEncodingParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Owin.StatelessAuth
{
/// <summary>
/// A query string or UrlEncoded form parser and editor
/// class that allows reading and writing of urlencoded
/// key value pairs used for query string and HTTP
/// form data.
///
/// Useful for parsing and editing querystrings inside
/// of non-Web code that doesn't have easy access to
/// the HttpUtility class.
/// </summary>
/// <remarks>
/// Supports multiple values per key
/// https://github.com/RickStrahl/WestwindToolkit/blob/master/Westwind.Utilities/SupportClasses/UrlEncodingParser.cs
/// </remarks>
internal class UrlEncodingParser : NameValueCollection
{
/// <summary>
/// Holds the original Url that was assigned if any
/// Url must contain // to be considered a url
/// </summary>
private string Url { get; set; }

/// <summary>
/// Determines whether plus signs in the UrlEncoded content
/// are treated as spaces.
/// </summary>
internal bool DecodePlusSignsAsSpaces { get; set; }

/// <summary>
/// Always pass in a UrlEncoded data or a URL to parse from
/// unless you are creating a new one from scratch.
/// </summary>
/// <param name="queryStringOrUrl">
/// Pass a query string or raw Form data, or a full URL.
/// If a URL is parsed the part prior to the ? is stripped
/// but saved. Then when you write the original URL is
/// re-written with the new query string.
/// </param>
internal UrlEncodingParser(string queryStringOrUrl = null, bool decodeSpacesAsPlusSigns = false)
{
Url = string.Empty;
DecodePlusSignsAsSpaces = decodeSpacesAsPlusSigns;
if (!string.IsNullOrEmpty(queryStringOrUrl))
{
Parse(queryStringOrUrl);
}
}

/// <summary>
/// Assigns multiple values to the same key
/// </summary>
/// <param name="key"></param>
/// <param name="values"></param>
internal void SetValues(string key, IEnumerable<string> values)
{
foreach (var val in values)
Add(key, val);
}

/// <summary>
/// Parses the query string into the internal dictionary
/// and optionally also returns this dictionary
/// </summary>
/// <param name="query">
/// Query string key value pairs or a full URL. If URL is
/// passed the URL is re-written in Write operation
/// </param>
/// <returns></returns>
internal NameValueCollection Parse(string query)
{
if (Uri.IsWellFormedUriString(query, UriKind.Absolute))
Url = query;

if (string.IsNullOrEmpty(query))
Clear();
else
{
int index = query.IndexOf('?');
if (index > -1)
{
if (query.Length >= index + 1)
query = query.Substring(index + 1);
}

var pairs = query.Split('&');
foreach (var pair in pairs)
{
int index2 = pair.IndexOf('=');
if (index2 > 0)
{
var val = pair.Substring(index2 + 1);
if (!string.IsNullOrEmpty(val))
{
if (DecodePlusSignsAsSpaces)
val = val.Replace("+", " ");
val = Uri.UnescapeDataString(val);
}

Add(pair.Substring(0, index2), val);
}
}
}

return this;
}

/// <summary>
/// Writes out the urlencoded data/query string or full URL based
/// on the internally set values.
/// </summary>
/// <returns>urlencoded data or url</returns>
public override string ToString()
{
string query = string.Empty;
foreach (string key in Keys)
{
string[] values = GetValues(key);
foreach (var val in values)
{
query += key + "=" + Uri.EscapeUriString(val) + "&";
}
}
query = query.Trim('&');

if (!string.IsNullOrEmpty(Url))
{
if (Url.Contains("?"))
query = Url.Substring(0, Url.IndexOf('?') + 1) + query;
else
query = Url + "?" + query;
}

return query;
}
}
}