dog-fooding-our-api-authentication
Dog-fooding our API - Authentication
http://blog.mirajavora.com/authenticate-web-api-using-access-tokens
With fabrik (my portfolio and blogging startup) only weeks away from launch I'm excited to start blogging about "how we did it".
As we're dog-fooding our own API (or "consuming it" if you don't get that expression) one of the big technical challenges was how to handle authentication.
I wanted to keep our web applications as first class citizens of the API so connecting to our auth database directly was a big no no. When I first started building the API I implemented OAuth but soon realized that the protocol was overkill for our needs (we only need Authentication not delegated Authorization).
It was working on my OAuth implementation that let me to the great work by the guys at Thinktecture, in particularThinktecture.IdentityModel. In short, if you need to do anything concerning identity and access control on the .NET stack, you'll probably find it in this library.
One thing included in this library is an authentication handler for ASP.NET Web API that supports many authentication types. One of these is support for session token authentication - a JSON Web Token (JWT) that can be used to achieve cookieless sessions with ASP.NET Web API.
Configuring the AuthenticationHandler
is just a matter of creating an AuthenticationConfiguration
and adding the authentication types your require:
private static void ConfigureAuth(HttpConfiguration config)
{
var authConfig = new AuthenticationConfiguration
{
DefaultAuthenticationScheme = "Basic",
EnableSessionToken = true,
SendWwwAuthenticateResponseHeader = true,
RequireSsl = false // only for testing
};
authConfig.AddBasicAuthentication((username, password) =>
{
return username == "admin" && password == "password";
});
config.MessageHandlers.Add(new AuthenticationHandler(authConfig));
}
Note the EnableSessionToken
setting. It is this that enables session token support.
Requesting a session token
To request a session token we need to make an authenticated request to the "/token" endpoint.
GET http://localhost:65104/token HTTP/1.1
User-Agent: Fiddler
Authorization: Basic YWRtaW46cGFzc3dvcmQ=
Host: localhost:65104
Here we are using Basic authentication. If authentication is successful we will be returned a JSON Web Token like so (shortened for brevity):
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjEz...",
"expires_in": 36000.0
}
This token is signed by the server so that it may be validated in subsequent requests.
Note that the signing key is re-created each time your application starts so if you're in a server farm scenario you may to provide your own key:
var authConfig = new AuthenticationConfiguration
{
DefaultAuthenticationScheme = "Basic",
EnableSessionToken = true, // default lifetime is 10 hours
SessionToken = new SessionTokenConfiguration
{
SigningKey = Constants.SessionTokenSigningKey
}
};
Now that we have a session token we can use it to authenticate requests:
GET http://localhost:65104/contacts HTTP/1.1
User-Agent: Fiddler
Authorization: Session eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjEz...
Host: localhost:65104
Accept: application/json
With this in place we can configure our Web Client (in this case ASP.NET MVC) to use the API to authenticate a user log in. It's as simple as making a request to "/token" with the provided user credentials:
[HttpPost, ValidateAntiForgeryToken]
public async Task<ActionResult> LogIn(UsersLogInModel model)
{
if (!ModelState.IsValid)
{
return View();
}
using (var httpClient = new HttpClient())
{
httpClient.BaseAddress = new Uri(Constants.ApiBaseUri);
httpClient.SetBasicAuthentication(model.Username, model.Password);
var response = await httpClient.GetAsync("token");
if (response.IsSuccessStatusCode)
{
var tokenResponse = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(tokenResponse);
var token = json["access_token"].ToString();
Session[Constants.SessionTokenKey] = token;
FormsAuthentication.SetAuthCookie(model.Username, createPersistentCookie: true);
return Redirect("~/");
}
else // could check specific error code here
{
ModelState.AddModelError("", "The username and password provided do not match any accounts on record.");
return View();
}
}
}
If the request is successful we parse the token from the response and store it in a session (you may need something a little more persistent). If the response is not successful then we assume the credentials are incorrect - of course this isn't necessarily the case (your server may have died) in which case you'd probably want to check the specific status code (401 if authentication failed).
The great thing about this approach is we've not had to store the user's credentials - it's like a simple man's version of the OAuth password grant flow.
One thing you'll have to watch out for is session token expiry. The easiest way round this is to ensure that your forms authentication cookie expires before the token (which defaults to 10 hours).
There's also the possibility (if using ASP.NET session to store your token) that your cookie may outlive your ASP.NET session, for example, if the web application restarts. We handle this with a global action filter that checks if the user is authenticated and whether a token exists (logging them out if not).
Authenticating JavaScript clients
When I first created the fabrik "dashboard" it was a typical data entry style application. This meant that all API access was done using via my controllers - something that the new async/await syntax in C# 5 and asynchronous controller actions in MVC4 made very easy indeed.
However as we started to focus on the user experience we began pushing more behavior into the client, usingKnockoutJS to build our UI - this meant making AJAX requests.
At first I was proxying everything via the MVC app like so:
[HttpPost]
public async Task<ActionResult> Move(int id, int position)
{
await portfolioClient.MoveProjectAsync(Customer.CurrentSite, new MoveProjectCommand
{
ProjectId = id,
Position = position
});
return Json(true);
}
As I started to do this more and more I realized this was just a pointless abstraction and if anything made my life harder.
What would be much better was if we could just call our API directly from JavaScript.
Enter CORS
CORS (short for Cross-Origin resource sharing) essentially provides a way to make AJAX requests to another domain. I'm not going to go into much more detail than that as you can read more here.
It requires two things:
- A client (browser) that supports making CORS requests.
- A server that issues the necessary Access-Control-* headers (and can handle pre-flight requests if neccessary).
If you need to handle more than GET/POST requests in addition to other headers (such as Authorization) you'll need to configure your server to respond to pre-flight requests. This is an OPTIONS request made by the browser to determine what is "allowed" by your server and where from. An example request and response can be seen below:
Request:
Access-Control-Request-Headers:accept, authorization, origin
Access-Control-Request-Method:GET
Host:localhost:65104
Origin:http://localhost:65306
Referer:http://localhost:65306/
Response:
Access-Control-Allow-Headers:accept,origin,authorization
Access-Control-Allow-Origin:*
Adding CORS support to ASP.NET Web API
Adding basic CORS support is pretty easy, using a simple message handler to response to OPTIONS requests and add the necessary headers. However, fine grained control requires more work but yet again you don't have to do this thanks to the guys at Thinktecture. You can read more on Brock Allen's introductory post.
Brock has created a nice fluent interface for configuring CORS - here's what I'm using to allow all origins, methods and request headers.
private static void ConfigureCors(HttpConfiguration config)
{
var corsConfig = new WebApiCorsConfiguration();
config.MessageHandlers.Add(new CorsMessageHandler(corsConfig, config));
corsConfig
.ForAllOrigins()
.AllowAllMethods()
.AllowAllRequestHeaders();
}
Putting it all together
Now that we've enabled CORS on the server we can make requests to our API directly from the browser.
The one missing piece is how we handle authentication. As discussed earlier we have access to a session token that can be used to authenticate requests. How can we make this available in the browser?
The solution I came up with was to generate a dynamic script using an MVC action that sets the token:
public class AssetsController : Controller
{
public ActionResult ApiAuth()
{
var token = Session[Constants.SessionTokenKey] as string;
var script = @"var my = my || {}; my.authToken = '" + token + "'; my.baseUri = '" + Constants.ApiBaseUri + "';";
return JavaScript(script);
}
}
This is then rendered in my layout page like so:
@if (Request.IsAuthenticated) { @Scripts.Render("~/assets/apiauth") }
I then created a simple wrapper client to make the API requests and set the appropriate authentication headers:
my.ApiClient = function (config) {
var authToken = config.authToken,
baseUri = config.baseUri,
configureRequest = function (xhr) {
xhr.setRequestHeader("Authorization", "Session " + authToken);
};
this.createUri = function (path) {
return baseUri + "/" + path;
};
this.get = function (path, query) {
return $.ajax({
url: this.createUri(path),
type: "GET",
beforeSend: configureRequest
});
};
this.post = function (path, data) {
return $.ajax({
url: this.createUri(path),
type: "POST",
contentType: "application/json",
dataType: "json",
data: data,
beforeSend: configureRequest
});
};
this.put = function (path, data) {
return $.ajax({
url: this.createUri(path),
type: "PUT",
contentType: "application/json",
dataType: "json",
data: data,
beforeSend: configureRequest
});
};
this.delete = function (path) {
return $.ajax({
url: this.createUri(path),
type: "DELETE",
dataType: "json",
beforeSend: configureRequest
});
}
};
We can initialize the client like so, passing in the auth token and base URI that was set by our MVC script:
<script>
(function () {
var apiClient = new my.ApiClient({
baseUri: my.baseUri,
authToken: my.authToken,
onAuthFail: function () {
$(".error").show();
}
});
var vm = new my.ContactsViewModel(apiClient);
ko.applyBindings(vm);
vm.refresh();
})();
</script>
To prove that everything works as expected I've created a little sample application on GitHub, aptly namedApiDogFood.
You need to make sure both projects are set to start and after logging in (username: admin, password: password) you should be able to add, update or remove (specifically GET, PUT, POST and DELETE) from your browser.
Enjoy!
dog-fooding-our-api-authentication的更多相关文章
- web api authentication
最近在学习web api authentication,以Jwt为例, 可以这样理解,token是身份证,用户名和密码是户口本, 身份证是有有效期的(jwt 有过期时间),且携带方便(自己带有所有信息 ...
- 获取使用GitHub api和Jira api Authentication的方法
近段时间在搭建我司的用例管理平台,有如下需求: 1.需要根据项目--版本--轮次的形式来管理项目用例,用例统一保存在git工程. 2.执行用例时,如果用例执行失败,可以通过平台在Jira上提bug. ...
- Java EE 7 / JAX-RS 2.0: Simple REST API Authentication & Authorization with Custom HTTP Header--reference
REST has made a lot of conveniences when it comes to implementing web services with the already avai ...
- API Authentication Error: {"error":"invalid_client","message":"Client authentication failed"}
解决方法:https://github.com/laravel/passport/issues/221 In your oauth_clients table, do the values you h ...
- [PHP] 浅谈 Laravel Authentication 的 auth:api
auth:api 在 Laravel 的 Routing , Middleware , API Authentication 主题中都有出现. 一. 在 Routing 部分可以知道 auth:api ...
- 关于RESTFUL API 安全认证方式的一些总结
常用认证方式 在之前的文章REST API 安全设计指南与使用 AngularJS & NodeJS 实现基于 token 的认证应用两篇文章中,[译]web权限验证方法说明中也详细介绍,一般 ...
- Using JAAS Authentication in Java Clients---weblogic document
The following topics are covered in this section: JAAS and WebLogic Server JAAS Authentication Devel ...
- API认证方法一览
Open api authentication Amazon DigitalOcean Webchat Weibo QQ Amazon Web Services HMAC Hash Message A ...
- 用ASP.NET Core 2.1 建立规范的 REST API -- 保护API和其它
本文介绍如何保护API,无需看前边文章也能明白吧. 预备知识: http://www.cnblogs.com/cgzl/p/9010978.html http://www.cnblogs.com/cg ...
- ASP.NET Core API 接收参数去掉烦人的 [FromBody]
在测试ASP.NET Core API 项目的时候,发现后台接口参数为类型对象,对于PostMan和Ajax的Post方法传Json数据都获取不到相应的值,后来在类型参数前面加了一个[FromBody ...
随机推荐
- AppWidget应用(一)---创建一个appWidget
appWidget是显示的桌面上的小窗口程序,通过它可以达到用户与程序之间的交互. 下面我们来看下创建一个appWidget的步骤 一.首先在layout文件夹下创建一个appWidget的布局文件a ...
- Linux常用C函数---内存控制篇
函数讲解部分参考http://net.pku.edu.cn/~yhf/linux_c/ calloc(配置内存空间) 相关函数 malloc,free,realloc,brk 表头文件 #includ ...
- 一个简单java爬虫爬取网页中邮箱并保存
此代码为一十分简单网络爬虫,仅供娱乐之用. java代码如下: package tool; import java.io.BufferedReader; import java.io.File; im ...
- GCC 编译选项
http://www.cnblogs.com/xmphoenix/archive/2011/03/21/1989944.html GCC 编译选项(转) gcc提供了大量的警告选项,对代码中可能存在的 ...
- 【机房系统知识小结点系列】之遍历窗体中的控件,判断Text是否为空?
做机房系统时,几乎每个窗体中都会用到判断界面中的控件是否为空的情景.我们曾经是这样走来的: 第一版: 好处:对窗体界面中的Text等控件,逐一做判断,当用户输入某一项为空的时候,会议弹出框的形式,告诉 ...
- HDU 3081Marriage Match II(二分法+并检查集合+网络流量的最大流量)
职务地址:http://acm.hdu.edu.cn/showproblem.php? pid=3081 有一段时间没写最大流的题了,这题建图竟然想了好长时间... 刚開始是按着终于的最大流即是做多轮 ...
- Objective-C——判断对象等同性
无论我们使用什么语言,总是会出现需要判断两个对象是否相等的情况,OC当然也不例外.首先看一段代码: NSString *str1 = [[NSString alloc] initWithCString ...
- Matlab自己定义函数
Matlab提供了强大的函数库供用户调用,但也支持用户自定义函数.本文使用了范德堡大学教授Akos Ledeczi授课中的样例来一步步说明怎样在Matlab中自定义函数. 首先,在command wi ...
- 有用的jQuery布局插件推荐
网页设计中排版也是很重要的,但有些比较难的网页排版我们可以用一些jQuery来实现,今天文章中主要分享一些有用的jQuery布局插件,有类似Pinterest流布局插件.友荐的滑动提示块以及其它jQu ...
- 【斐波拉契+数论+同余】【ZOJ3707】Calculate Prime S
题目大意: S[n] 表示 集合{1,2,3,4,5.......n} 不存在连续元素的子集个数 Prime S 表示S[n]与之前的所有S[i]互质; 问 找到大于第K个PrimeS 能整除X 的第 ...