我有一个包含大量方法的Web API,所有方法都需要提供承载令牌才能使用。这些方法都从承载令牌中提取信息。
我想测试API在生成时是否正确填充了承载令牌。我正在使用Microsoft.Owin.Testing框架编写测试。我有一个看起来像这样的测试:
[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"));
}
}
因此,我能够检索表示令牌的字符串。但是现在我想实际访问该令牌的内容,并确保提供了某些声明。
我将在实际的身份验证方法中用于检查声明的代码如下所示:
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");
因此,我想做的就是在测试本身中提供承载令牌字符串并重新接收User.Identity对象,或者以其他方式访问令牌所包含的声明。这就是我要测试我的方法是否正确向令牌添加了必要声明的方式。
“幼稚”的方法可能是在我的API中编写一个方法,该方法仅返回给定的承载令牌中的所有声明。但感觉这是不必要的。在调用我的控制器的方法之前,ASP.NET会以某种方式将给定的令牌解码为一个对象。我想自己在测试代码中复制相同的操作。
能做到吗?如果是这样,怎么办?
编辑:我的OWIN启动类实例化了我编写的用于处理身份验证和令牌生成的身份验证令牌提供程序。在我的启动课程中,我有以下内容:
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);
}
这是我的OAuth提供者提供的相关代码:
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);
}
单元测试将要确保声明customData
实际上存在于身份验证令牌中。因此,我需要一种方法来评估提供的令牌以测试其中包含哪些令牌。
编辑2:我花了一些时间查看Katana源代码并在线搜索其他帖子,而且将这个应用程序托管在IIS上似乎很重要,因此我将使用SystemWeb。看起来SystemWeb对令牌使用了机器密钥加密。看起来AccessTokenFormat
选项中的参数在这里也很重要。
因此,现在我想知道的是,是否可以基于此知识实例化自己的“解码器”。假设我只会在IIS上托管,我可以实例化一个解码器,然后可以解码令牌并将其转换为Claims对象吗?
关于此的文档有点稀疏,并且代码似乎使您无所适从,很多事情试图让我直觉。
编辑3:我发现了一个项目,其中包含应该是不记名令牌反序列化器。我在其“ API”库中修改了代码,并一直试图使用它来解密由我的API生成的令牌。
我<machineKey...>
使用Microsoft的PowerShell脚本生成了一个值,并将其放置在API本身的Web.config文件和测试项目中的App.confg文件中。
但是,令牌仍然无法解密。我收到抛出的异常:System.Security.Cryptography.CryptographicException
消息"Error occurred during a cryptographic operation."
,以下是错误的堆栈跟踪:
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
在这一点上,我很沮丧。在整个项目中,将MachineKey值设置为相同时,我看不到为什么无法解密令牌。我猜想加密错误是故意模糊的,但是我不确定现在从哪里开始解决这个问题。
我要做的就是在单元测试中测试令牌是否包含所需的数据... :-)
我终于能够找到解决方案。我在Startup类中添加了一个公共变量,该变量公开了OAuthBearerAuthenticationOptions
传递给UseBearerTokenAuthentication
方法的对象。从该对象,我可以调用AccessTokenFormat.Unprotect
并获取解密的令牌。
我还重写了测试以单独实例化Startup类,以便可以从测试中访问值。
我仍然不明白为什么MachineKey东西不起作用,为什么我不能直接取消保护令牌。看起来,只要MachineKey匹配,我就应该能够解密令牌,甚至可以手动解密。但这至少似乎可行,即使它不是最佳解决方案。
这可能可以更干净地完成,例如Startup类可以以某种方式检测它是否正在测试中,并以其他某种方式将对象传递给测试类,而不是将其挂在外面。但是目前看来,这确实可以满足我的需求。
我的启动类通过以下方式公开变量:
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);
}
}
我的测试现在看起来像这样:
[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.
}
}
希望我所有的努力都能帮助其他尝试做类似事情的人。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句