ReactとかやっててWebAPIを呼び出してやるようにすると、認証ってのがどうしても必要になってきます。ということでASP.NET WebAPI2でOAuth認証を行ってAPIをたたくところまでやってみようと思います。
スタート地点は先日作ったReactApplicationの雛形です。
NuGetから必要なパッケージをインストールします。
- Microsoft.Owin.Host.SystemWeb
- Microsoft.AspNet.WebApi.Owin
- Microsoft.Owin.Security.OAuth
- Microsoft.AspNet.Identity.Owin
Owinで検索したら大体出てきます。
App_Startの中のWebApiConfig.csを修正してBearer token 認証のみを使用するように Web API を設定します
using Microsoft.Owin.Security.OAuth; using System.Web.Http; namespace OAuthSample { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API の設定およびサービス // 以下の2行を追加 config.SuppressDefaultHostAuthentication(); config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType)); // Web API ルート config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
次に、OWINのStartupクラスを作成します。アイテムテンプレートにあるのでさくっとプロジェクト直下に作りましょう。このクラスでOAuthの認証の設定を行います。
using System; using System.Threading.Tasks; using Microsoft.Owin; using Owin; using Microsoft.Owin.Security.OAuth; using System.Security.Claims; [assembly: OwinStartup(typeof(OAuthSample.Startup))] namespace OAuthSample { public class Startup { public void Configuration(IAppBuilder app) { app.UseOAuthBearerTokens(new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString("/Token"), Provider = new ApplicationOAuthProvider(), AccessTokenExpireTimeSpan = TimeSpan.FromDays(14), // 要件に応じて期限を AllowInsecureHttp = true }); } } class ApplicationOAuthProvider : OAuthAuthorizationServerProvider { public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { context.Validated(); return Task.FromResult(0); } public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { // 認証ロジックを書く if (context.UserName == "admin" && context.Password == "p@ssw0rd") { // context.Options.AuthenticationTypeを使ってClaimsIdentityを作る var identity = new ClaimsIdentity(context.Options.AuthenticationType); // 必要なClaimを追加しておく。 identity.AddClaims(new[] { new Claim(ClaimTypes.GivenName, context.UserName), new Claim(ClaimTypes.Role, "User"), new Claim(ClaimTypes.Role, "Admin") }); context.Validated(identity); } else { context.Rejected(); } return Task.FromResult(0); } } }
カスタム認証ロジックはApplicationOAuthProviderクラスのGrantResourceOwnerCredentialsで行います。認証したらClaimsIdentityを作って(このとき引数にはcontext.Options.AuthenticationTypeを渡すこと)Claimを追加してValidatedに渡します。
これで/TokenにPOSTで定型文を送ることで認証トークンが返されます。
認証が必要なAPIには、以下のようにAuthorize属性を付けます。ClaimでRoleを設定してたらRoleによる認証も可能です。
using System.Web.Http; namespace OAuthSample.Controllers { [Authorize] public class CalcController : ApiController { public IHttpActionResult Get(int x, int y) { return Ok(x + y); } } }
あとはAjaxで呼び出したりするだけです。
import * as React from 'react'; interface IndexPageProps extends React.Props<{}> { } interface IndexPageState { answer?: number; accessToken?: string; } export default class IndexPage extends React.Component<IndexPageProps, IndexPageState> { constructor(props: IndexPageProps) { super(props); this.state = { answer: 0, accessToken: '' }; } private handleSubmit(e: React.SyntheticEvent) { e.preventDefault(); var x = (this.refs['x'] as HTMLInputElement).value; var y = (this.refs['y'] as HTMLInputElement).value; // 認証ヘッダーを追加してAPI呼び出し fetch('api/Calc?x=' + x + '&y=' + y, { headers: { 'Authorization': 'Bearer ' + this.state.accessToken } }).then(x => { if (x.status !== 200) { throw new Error(); } return x.json(); }).then((x: number) => { this.setState({ answer: x }); }).catch(_ => { alert('Error'); }); } private handleAuthClick(e: React.SyntheticEvent) { e.preventDefault(); // 認証してアクセストークンを保持しておく fetch('Token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'grant_type=password&username=admin&password=p@ssw0rd' }).then(x => { return x.json(); }).then(x => { this.setState({ accessToken: x.access_token }); });; } render() { return ( <form onSubmit={this.handleSubmit.bind(this) }> <input type='text' ref='x' /> + <input type='text' ref='y' /> <input type='submit' value='=' /> <span>{this.state.answer}</span> <hr/> <button onClick={this.handleAuthClick.bind(this) }>Auth</button> <br/> <span>{this.state.accessToken}</span> </form> ); } }
Tokenに対してPOSTでContent-Typeをapplication/x-www-form-urlencodedにして、bodyにgrant_type=password&username=ユーザ名&password=パスワードの形で渡してやれば認証トークンが返ってきます。JSONのaccess_tokenを保存しておけばOKです。
その他に、有効期限なんかも返ってくるので、必要に応じて保持しておくといいと思います。
そして、ここで取得したアクセストークンをAuthorizationヘッダーにBearer トークンの形でくっつけてAPIを呼び出すと認証が必要なAPIを呼び出せます。
ソースコード
ソースはGitHubに上げておきます。
参考サイト
参考にしたサイトです。
ASP.NET SPA (JavaScript) の Web API 認証 (ASP.NET Identity) | 松崎 剛 Blog
Token Based Authentication using ASP.NET Web API 2, Owin, and Identitybitoftech.net