Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper way to test authenticated methods (using bearer tokens) in C# (web api)

I have a Web API with tons of methods that all require a bearer token to be present in order to be used. These methods all extract information from the bearer token.

I want to test whether the API is properly populating the bearer token upon its generation. I'm using the Microsoft.Owin.Testing framework to write my tests. I have a test that looks like this:

[TestMethod]
public async Task test_Login() 
{
    using (var server = TestServer.Create<Startup>())
    {
        var req = server.CreateRequest("/authtoken");
        req.AddHeader("Content-Type", "application/x-www-form-urlencoded");
        req.And(x => x.Content = new StringContent("grant_type=password&username=test&password=1234", System.Text.Encoding.ASCII));
        var response = await req.GetAsync();

        // Did the request produce a 200 OK response?
        Assert.AreEqual(response.StatusCode, System.Net.HttpStatusCode.OK);

        // Retrieve the content of the response
        string responseBody = await response.Content.ReadAsStringAsync();
        // this uses a custom method for deserializing JSON to a dictionary of objects using JSON.NET
        Dictionary<string, object> responseData = deserializeToDictionary(responseBody); 

        // Did the response come with an access token?
        Assert.IsTrue(responseData.ContainsKey("access_token"));

    }
}

So I'm able to retrieve the string that represents the token. But now I want to actually access that token's contents, and make sure that certain claims were provided.

Code that I would use in an actual authenticated method to check the claims looks like this:

var identity = (ClaimsIdentity)User.Identity;
IEnumerable<Claim> claims = identity.Claims;

var claimTypes = from x in claims select x.Type;

if (!claimTypes.Contains("customData"))
    throw new InvalidOperationException("Not authorized");

So what I want to be able to do is, within my test itself, provide the bearer token string and reeceive a User.Identity object or in some other way gain access to the claims that token contains. This is how I want to test whether my method is properly adding the necessary claims to the token.

The "naive" approach could be to write a method in my API that simply returns all the claims in the bearer token it is given. But it feels like this should be unnecessary. ASP.NET is somehow decoding the given token to an object before my controller's method is called. I want to replicate the same action on my own, in my test code.

Can this be done? If so, how?


EDIT: My OWIN startup class instantiates an authentication token provider that I have coded which handles authentication and token generation. In my startup class I have this:

public void Configuration(IAppBuilder app)
{
    // Setup configuration object
    HttpConfiguration config = new HttpConfiguration();

    // Web API configuration and services
    // Configure Web API to use only bearer token authentication.
    config.SuppressDefaultHostAuthentication();
    config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

    // Web API routes
    config.MapHttpAttributeRoutes();
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

    // configure the OAUTH server
    OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
    {
        //AllowInsecureHttp = false,
        AllowInsecureHttp = true, // THIS HAS TO BE CHANGED BEFORE PUBLISHING!

        TokenEndpointPath = new PathString("/authtoken"),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
        Provider = new API.Middleware.MyOAuthProvider()
    };

    // Now we setup the actual OWIN pipeline.

    // setup CORS support
    // in production we will only allow from the correct URLs.
    app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);

    // Token Generation
    app.UseOAuthAuthorizationServer(OAuthServerOptions);
    app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

    // insert actual web API and we're off!
    app.UseWebApi(config);
}

Here is the relevant code from my OAuth provider:

public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{

    // Will be used near end of function
    bool isValidUser = false;

    // Simple sanity check: all usernames must begin with a lowercase character
    Match testCheck = Regex.Match(context.UserName, "^[a-z]{1}.+$");
    if (testCheck.Success==false)
    {
        context.SetError("invalid_grant", "Invalid credentials.");
        return;
    }

    string userExtraInfo;
    // Here we check the database for a valid user.
    // If the user is valid, isValidUser will be set to True.
    // Invalid authentications will return null from the method below.
    userExtraInfo = DBAccess.getUserInfo(context.UserName, context.Password);
    if (userExtraInfo != null) isValidUser = true;

    if (!isValidUser)
    {
        context.SetError("invalid_grant", "Invalid credentials.");
        return;
    }

    // The database validated the user. We will include the username in the token.
    string userName = context.UserName;

    // generate a claims object
    var identity = new ClaimsIdentity(context.Options.AuthenticationType);

    // add the username to the token
    identity.AddClaim(new Claim(ClaimTypes.Sid, userName));

    // add the custom data on the user to the token.
    identity.AddClaim(new Claim("customData", userExtraInfo));

    // store token expiry so the consumer can determine expiration time
    DateTime expiresAt = DateTime.Now.Add(context.Options.AccessTokenExpireTimeSpan);
    identity.AddClaim(new Claim("expiry", expiresAt.ToString()));

    // Validate the request and generate a token.
    context.Validated(identity);

}

The unit test would want to ensure that the customData claim is in fact present in the authentication token. So thus my need for a way to evaluate the token provided to test which claims it contains.


EDIT 2: I've spent some time looking over the Katana source code and searching out some other posts online, and it looks like it's important that I'm hosting this app on IIS, so I would be using SystemWeb. It looks like SystemWeb uses Machine Key encryption for the token. It also looks like the AccessTokenFormat parameter in the options is relevant here.

So now what I'm wondering is if I can instantiate my own "decoder" based on this knowledge. Assuming I will only ever be hosting on IIS, can I instantiate a decoder that can then decode the token and convert it into a Claims object?

The docs on this are kind of sparse and the code seems to throw you all over the place, a lot to try to keep straight in my head.


EDIT 3: I found a project that contains what is supposed to be a bearer token deserializer. I adapted the code in its "API" library and have been trying to use it to decrypt the tokens generated by my API.

I generated a <machineKey...> value using a PowerShell script from Microsoft and placed it both in the Web.config file of the API itself and the App.confg file in the test project.

The tokens still fail to decrypt, however. I receive a Exception thrown: System.Security.Cryptography.CryptographicException with the message "Error occurred during a cryptographic operation." The following is the stacktrace of the error:

at System.Web.Security.Cryptography.HomogenizingCryptoServiceWrapper.HomogenizeErrors(Func`2 func, Byte[] input)
at System.Web.Security.Cryptography.HomogenizingCryptoServiceWrapper.Unprotect(Byte[] protectedData)
at System.Web.Security.MachineKey.Unprotect(ICryptoServiceProvider cryptoServiceProvider, Byte[] protectedData, String[] purposes)
at System.Web.Security.MachineKey.Unprotect(Byte[] protectedData, String[] purposes)
at MyAPI.Tests.BearerTokenAPI.MachineKeyDataProtector.Unprotect(Byte[] protectedData) in D:\Source\MyAPI\MyAPI.WebAPI.Tests\BearerTokenAPI.cs:line 251
at MyAPI.Tests.BearerTokenAPI.SecureDataFormat`1.Unprotect(String protectedText) in D:\Source\MyAPI\MyAPI.WebAPI.Tests\BearerTokenAPI.cs:line 287

At this point I'm stumped. With the MachineKey value set to the same across the entire project, I don't see why I'm unable to decrypt the tokens. I'm guessing the cryptographic error is being deliberately vague, but I am not sure where to start with figuring this out now.

And all I wanted to do was test that the token contains the desired data in a unit test.... :-)

like image 458
fdmillion Avatar asked Apr 07 '17 00:04

fdmillion


People also ask

How do I authenticate using Bearer Token?

Bearer tokens enable requests to authenticate using an access key, such as a JSON Web Token (JWT). The token is a text string, included in the request header. In the request Authorization tab, select Bearer Token from the Type dropdown list. In the Token field, enter your API key value.

How does bearer authentication work?

The Bearer Token is created for you by the Authentication server. When a user authenticates your application (client) the authentication server then goes and generates for you a Token. Bearer Tokens are the predominant type of access token used with OAuth 2.0.

What type of authentication is Bearer Token?

Bearer authentication (also called token authentication) is an HTTP authentication scheme that involves security tokens called bearer tokens.

How is Bearer Token validation?

Bearer Token Types That type of bearer token cannot be validated by the Resource Server without direct communication with an Authorization Server. JWT Token represents the JSON object with statements (claims) about the user and token.


1 Answers

I was finally able to figure out a solution. I added a public variable to my Startup class that exposes the OAuthBearerAuthenticationOptions object passed into the UseBearerTokenAuthentication method. From that object, I'm able to call AccessTokenFormat.Unprotect and get a decrypted token.

I also rewrote my test to instantiate the Startup class separately, so that I have access to the value from within the test.

I still don't understand why the MachineKey thing isn't working, why I'm not able to directly unprotect the token. It would seem that as long as the MachineKey's match, I should be able to decrypt the token, even manually. But at least this seems to work, even if it's not the best solution.

This could probably be done more cleanly, for instance perhaps the Startup class could somehow detect if it's being started under test and pass the object to the test class in some other fashion rather than leaving it hanging out there in the breeze. But for now this seems to do exactly what I needed.

My startup class exposes the variable this way:

public partial class Startup
{
    public OAuthBearerAuthenticationOptions oabao;

    public void Configuration(IAppBuilder app)
    {

        // repeated code omitted

        // Token Generation
        app.UseOAuthAuthorizationServer(OAuthServerOptions);
        oabao = new OAuthBearerAuthenticationOptions();
        app.UseOAuthBearerAuthentication(oabao);

        // insert actual web API and we're off!
        app.UseWebApi(config);

    }
}

My test now looks like this:

[TestMethod]
public async Task Test_SignIn()
{
    Startup owinStartup = new Startup();
    Action<IAppBuilder> owinStartupAction = new Action<IAppBuilder>(owinStartup.Configuration);

    using (var server = TestServer.Create(owinStartupAction))
    {
        var req = server.CreateRequest("/authtoken");
        req.AddHeader("Content-Type", "application/x-www-form-urlencoded");

        // repeated code omitted

        // Is the access token of an appropriate length?
        string access_token = responseData["access_token"].ToString();
        Assert.IsTrue(access_token.Length > 32);

        AuthenticationTicket token = owinStartup.oabao.AccessTokenFormat.Unprotect(access_token);

        // now I can check whatever I want on the token.
    }
}

Hopefully all my efforts will help someone else trying to do something similar.

like image 96
fdmillion Avatar answered Sep 26 '22 11:09

fdmillion