Skip to content

Commit

Permalink
adds support for basic (client+server) and bearer tokens (only client…
Browse files Browse the repository at this point in the history
… side)
  • Loading branch information
pmhsfelix committed Mar 11, 2013
1 parent aa75e73 commit d34fe15
Show file tree
Hide file tree
Showing 18 changed files with 679 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
[Bb]in/
[Oo]bj/

# Secret Credentials Folder
SecretCredentials/

# mstest test results
TestResults

Expand Down
6 changes: 6 additions & 0 deletions src/WebApiBook.IssueTrackerApi.sln
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{23BA1D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiBook.IssueTrackerApi.Fakes", "WebApiBook.IssueTrackerApi.Fakes\WebApiBook.IssueTrackerApi.Fakes.csproj", "{D96033F9-F76D-4DD0-A267-52094C0F25CB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiBook.IssueTrackerApi.IntegrationTests", "..\test\WebApiBook.IssueTrackerApi.IntegrationTests\WebApiBook.IssueTrackerApi.IntegrationTests.csproj", "{15531DF5-1085-4056-86EF-0AC820F8A2DB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -32,6 +34,10 @@ Global
{D96033F9-F76D-4DD0-A267-52094C0F25CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D96033F9-F76D-4DD0-A267-52094C0F25CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D96033F9-F76D-4DD0-A267-52094C0F25CB}.Release|Any CPU.Build.0 = Release|Any CPU
{15531DF5-1085-4056-86EF-0AC820F8A2DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15531DF5-1085-4056-86EF-0AC820F8A2DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{15531DF5-1085-4056-86EF-0AC820F8A2DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15531DF5-1085-4056-86EF-0AC820F8A2DB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web.Mvc;

namespace WebApiBook.IssueTrackerApi.Infrastructure
{
public class BasicAuthnClientHandler : DelegatingHandler
{
private readonly Func<HttpRequestMessage, Task<BasicCredentials>> _prov;

public BasicAuthnClientHandler(Func<HttpRequestMessage, Task<BasicCredentials>> prov)
{
_prov = prov;
}

protected override async Task<HttpResponseMessage>
SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
var creds = await _prov(request);
request.Headers.Authorization = new AuthenticationHeaderValue(
"Basic", creds.ToCredentialString());
return await base.SendAsync(request, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using System.Web;

namespace WebApiBook.IssueTrackerApi.Infrastructure
{
public class BasicAuthnServerHandler : DelegatingHandler
{
private readonly string _realm;
private readonly Func<BasicCredentials, Task<bool>> _val;
private readonly Func<HttpRequestMessage, Task<BasicCredentials>> _prov;

public BasicAuthnServerHandler(string realm, Func<BasicCredentials, Task<bool>> val)
{
_realm = realm;
_val = val;
}

protected override async Task<HttpResponseMessage>
SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
BasicCredentials creds;
if (request.Headers.Authorization != null
&& request.Headers.Authorization.Scheme.Equals("Basic", StringComparison.InvariantCultureIgnoreCase)
&& (creds = BasicCredentials.TryParse(request.Headers.Authorization.Parameter)) != null)
{
if(!await _val(creds))
{
return UnAuthorizedResponseMessage();
}
SetPrincipal(request, creds);
return await base.SendAsync(request, cancellationToken);
}
var resp = new HttpResponseMessage(HttpStatusCode.Unauthorized);
resp.Headers.WwwAuthenticate.Add(
new AuthenticationHeaderValue("Basic", string.Format("realm={0}", _realm)));
return resp;
}

private static void SetPrincipal(HttpRequestMessage request, BasicCredentials creds)
{
var princ = new ClaimsPrincipal(new ClaimsIdentity(
new Claim[] {new Claim(ClaimTypes.NameIdentifier, creds.Username),}
));
Thread.CurrentPrincipal = princ;
if(HttpContext.Current != null)
{
HttpContext.Current.User = princ;
}
}

private HttpResponseMessage UnAuthorizedResponseMessage()
{
var resp = new HttpResponseMessage(HttpStatusCode.Unauthorized);
resp.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Basic",string.Format("realm= {0}",_realm)));
return resp;
}
}
}
42 changes: 42 additions & 0 deletions src/WebApiBook.IssueTrackerApi/Infrastructure/BasicCredentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Text;

namespace WebApiBook.IssueTrackerApi.Infrastructure
{
public class BasicCredentials
{
public string Username { get; private set; }
public string Password { get; private set; }

public BasicCredentials(string username, string password)
{
Username = username;
Password = password;
}

public static BasicCredentials TryParse(string credentials)
{
string pair;
try
{
pair = Encoding.ASCII.GetString(Convert.FromBase64String(credentials));
}
catch (FormatException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
var ix = pair.IndexOf(':');
return ix == -1 ? null : new BasicCredentials(pair.Substring(0, ix), pair.Substring(ix + 1));
}

public string ToCredentialString()
{
return Convert.ToBase64String(
Encoding.ASCII.GetBytes(Username + ':' + Password));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;

namespace WebApiBook.IssueTrackerApi.Infrastructure
{
public class BearerAuthnClientHandler : DelegatingHandler
{
private readonly Func<HttpRequestMessage, Task<BearerToken>> _prov;

public BearerAuthnClientHandler(Func<HttpRequestMessage, Task<BearerToken>> prov)
{
_prov = prov;
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
var token = await _prov(request);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer",token.Value);
return await base.SendAsync(request, cancellationToken);
}
}
}
12 changes: 12 additions & 0 deletions src/WebApiBook.IssueTrackerApi/Infrastructure/BearerToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace WebApiBook.IssueTrackerApi.Infrastructure
{
public class BearerToken
{
public string Value { get; private set; }

public BearerToken(string value)
{
Value = value;
}
}
}
78 changes: 78 additions & 0 deletions src/WebApiBook.IssueTrackerApi/Infrastructure/GitHubTokenClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;

namespace WebApiBook.IssueTrackerApi.Infrastructure
{
public class GitHubTokenClient
{
private readonly string _clientId;
private readonly string _clientSecret;

public GitHubTokenClient(string clientId, string clientSecret)
{
_clientId = clientId;
_clientSecret = clientSecret;
}

public async Task<GitHubToken> GetBearerTokenAsync(BasicCredentials creds, string[] scopes)
{
using(var client = new HttpClient(new BasicAuthnClientHandler(req => Task.FromResult(creds))
{
InnerHandler = new HttpClientHandler()
}))
{
var resp = await client.PostAsJsonAsync("https://api.github.com/authorizations", new TokenRequestModel
{
scopes = scopes,
client_id = _clientId,
client_secret = _clientSecret
});
resp.EnsureSuccessStatusCode();
var respModel = await resp.Content.ReadAsAsync<TokenResponseModel>();
return new GitHubToken(respModel.token, respModel.url);
}
}

public async Task<HttpStatusCode> DeleteBearerTokenAsync(BasicCredentials creds, string url)
{
using (var client = new HttpClient(new BasicAuthnClientHandler(req => Task.FromResult(creds))
{
InnerHandler = new HttpClientHandler()
}))
{
return (await client.DeleteAsync(url)).StatusCode;
}
}
}

public class GitHubToken
{
public string Value { get; private set; }
public string Uri { get; private set; }

public GitHubToken(string value, string uri)
{
Value = value;
Uri = uri;
}
}

public class TokenRequestModel
{
public string[] scopes { get; set; }
public string client_id { get; set; }
public string client_secret { get; set; }
}

public class TokenResponseModel
{
public int id { get; set; }
public string url { get; set; }
public string token { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,13 @@
<Compile Include="Global.asax.cs">
<DependentUpon>Global.asax</DependentUpon>
</Compile>
<Compile Include="Infrastructure\BasicAuthnClientHandler.cs" />
<Compile Include="Infrastructure\BasicAuthnServerHandler.cs" />
<Compile Include="Infrastructure\BasicCredentials.cs" />
<Compile Include="Infrastructure\BearerAuthnClientHandler.cs" />
<Compile Include="Infrastructure\BearerToken.cs" />
<Compile Include="Infrastructure\GithubIssueSource.cs" />
<Compile Include="Infrastructure\GitHubTokenClient.cs" />
<Compile Include="Infrastructure\IIssueSource.cs" />
<Compile Include="Infrastructure\IssueSource.cs" />
<Compile Include="Infrastructure\UriExtensions.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using WebApiBook.IssueTrackerApi.Infrastructure;
using WebApiBook.IssueTrackerApi.IntegrationTests.GitHub.SecretCredentials;
using Xunit;

namespace WebApiBook.IssueTrackerApi.IntegrationTests.GitHub
{
public class BasicAuthnClientHandlerTests
{
public class TheBasicAuthnClientHandler
{
[Fact]
public async Task ShouldAuthenticateOutboundGitHubRequests()
{
const string username = TestSecretCredentials.UserName;
const string password = TestSecretCredentials.Password;
using(var client = new HttpClient(
new BasicAuthnClientHandler(req => Task.FromResult(new BasicCredentials(username, password)))
{
InnerHandler = new HttpClientHandler()
}))
{
var resp = await client.GetAsync("https://api.github.com/orgs/webapibook/issues");
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using WebApiBook.IssueTrackerApi.Infrastructure;
using WebApiBook.IssueTrackerApi.IntegrationTests.GitHub.SecretCredentials;
using Xunit;

namespace WebApiBook.IssueTrackerApi.IntegrationTests.GitHub
{
public class GitHubTokenClientTests
{
public class TheGitHubTokenClient
{
[Fact]
public async Task ShouldCreateOAuthTokens()
{
const string username = TestSecretCredentials.UserName;
const string password = TestSecretCredentials.Password;
const string clientId = TestSecretCredentials.ClientId;
const string clientSecret = TestSecretCredentials.ClientSecret;
var tokenClient = new GitHubTokenClient(clientId, clientSecret);
var token = await tokenClient.GetBearerTokenAsync(new BasicCredentials(username, password), new string[] {"repo"});
try
{
using (
var client =
new HttpClient(
new BearerAuthnClientHandler(req => Task.FromResult(new BearerToken(token.Value)))
{
InnerHandler = new HttpClientHandler()
}))
{
var httpResp = await client.GetAsync("https://api.github.com/orgs/webapibook/issues");
Assert.Equal(HttpStatusCode.OK, httpResp.StatusCode);
}
}finally
{
tokenClient.DeleteBearerTokenAsync(new BasicCredentials(username, password), token.Uri);
}
}
}
}
}
Loading

0 comments on commit d34fe15

Please sign in to comment.