読者です 読者をやめる 読者になる 読者になる

かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

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

ASP.NET 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