かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

ASP.NET WebAPI2でAjaxでOAuth認証するよ(ついでにTypeScriptとReact)

ReactとかやっててWebAPIを呼び出してやるようにすると、認証ってのがどうしても必要になってきます。ということでASP.NET WebAPI2でOAuth認証を行ってAPIをたたくところまでやってみようと思います。

スタート地点は先日作ったReactApplicationの雛形です。

blog.okazuki.jp

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' />
                &nbsp;
                +
                &nbsp;
                <input type='text' ref='y' />
                &nbsp;
                <input type='submit' value='=' />
                &nbsp;
                <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を呼び出せます。

f:id:okazuki:20160115215405p:plain

ソースコード

ソースはGitHubに上げておきます。

github.com

参考サイト

参考にしたサイトです。

ASP.NET SPA (JavaScript) の Web API 認証 (ASP.NET Identity) | 松崎 剛 Blog

timney.net

www.asp.net

Token Based Authentication using ASP.NET Web API 2, Owin, and Identity
bitoftech.net