Quantcast
Channel: IdM実験室
Viewing all articles
Browse latest Browse all 770

[WAAD]OAuth で WebAPI を保護する ①

$
0
0
Windows Azure Active Directory(WAAD)が OAuth2.0 / OpenID Connect に対応しつつある、という話は以前紹介しましたが、具体的にどうやって使うのか?についてはあまり解説記事もないので、今回簡単に紹介していこうと思います。
(一部、Preview の機能を使うので今後手順などが変更になる可能性があります)

参考)[WAAD] OAuth2.0 への対応状況まとめ&ちょこっと OpenID Connect も
    http://idmlab.eidentity.jp/2013/07/waad-oauth20-openid-connect.html

◆やりたいこと
WebAPI へのアクセスを WAAD の OAuth2.0 の機能を使って保護する
⇒つまり、WAAD が発行したアクセストークンを使って WebAPI の実行の認可をしてみようと思います。

簡単に絵にすると以下のようになります。


ポイントは、ユーザが直接 WebAPI を実行するのではなく、OAuth クライアントが実行する、かつその際に OAuth クライアントに ID/PWD を設定しておかなくてもユーザの代理として機能する、というところです。


◆作業の流れ
作業の流れは以下の通りです。
  1. WebAPI の作成:今回は ASP.NET MVC4 の WebAPI を使います
  2. WebAPI の保護:WAAD を使って認可する様に設定を行います
  3. WebAPI を WAAD に登録:WAAD の保護対象リソースとして WAAD へ登録します
  4. OAuth クライアントの作成:本来は真面目にクライアントも作るのですが、今回は生の動きを見るために Chrome Extension の Advanced REST Client とダミー URL を使います
  5. OAuth クライアントを WAAD に登録:WAAD を使うアプリケーションとして OAuth クライアントを登録します
  6. WebAPI へのアクセス許可設定:OAuth クライアントが WebAPI へアクセスできるように設定します
  7. 動作確認:実際に OAuth2.0 のフローに従ってアクセスできるかどうかテストします。(今回は認可コードフローを試してみます)
ちょっと長めなので、2,3回に分割して紹介していきます。


◆実際の作業
では、始めます。

1.WebAPI の作成

Visual Studio 2012 で以下の通り WebAPI を作成します。ここで作成した WebAPI へのアクセスを Windows Azure Active Directory(WAAD)の OAuth を使って保護します。

まずは、ASP.NET MVC4 Web アプリケーションを作成します。



プロジェクトテンプレートでは WebAPI を選択します。



作成が終わったら F5 を押してデバッグモードで起動します。
ブラウザで http://localhost:[アサインされたポート番号]/Api/Values へアクセスすると Visual Studio のテンプレートに登録されている値が表示されます。


2.WebAPI の保護

作成した WebAPI を WAAD で保護するための設定を行います。具体的には Authorization ヘッダに設定されてくるアクセストークンの Validation をするために、global.asax にValidationHandler を作成、登録します。これでトークンの Validation に失敗すると HTTP 401 Unauthorized が返るようになります。

まず必要なライブラリへの参照を設定します。必要なのは、以下の3つです。
  • System.IdentityModel
  • JSON Web Token Handler for the Microsoft .NET Framework(NuGet パッケージ)
  • Microsoft Token Validation Extension for Microsoft .NET Framework 4.5(NuGet パッケージ)
System.IdentityModel

JSON Web Token Handler for the Microsoft .NET Framework

Microsoft Token Validation Extension for Microsoft .NET Framework 4.5



参照設定が終わったら、global.asax へ TokenValidationHandler を追加します。
この部分は MSDN の以下のサイトに紹介されているので、global.asax のソースコードはそのまま使います。
 Securing a Windows Store Application and REST Web Service Using Windows Azure AD (Preview)

環境によって変えるのは、以下の2点だけです。
 const string domainName = “xxx.onmicrosoft.com";
  ※契約した WAAD のテナントドメイン名
 const string audience = “http://localhost:[ポート番号]";
  ※作成した WebAPI の URI


一応ソースを張り付けておきます。

using System;
using System.Collections.Generic;
using System.IdentityModel.Metadata;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using System.Xml;
using System.Xml.Linq;

namespace ProtectedAPI
{
// メモ: IIS6 または IIS7 のクラシック モードの詳細については、
// http://go.microsoft.com/?LinkId=9394801 を参照してください

public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();

WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);

// Add Token Validation Handler -- 20131020
GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());
}
}

// Token Validation Handler Class -- 20131020
internal class TokenValidationHandler : DelegatingHandler
{
// Domain name or Tenant name
const string domainName = "xxxx.onmicrosoft.com";
const string audience = "http://localhost:52941";

static DateTime _stsMetadataRetrievalTime = DateTime.MinValue;
static List<X509SecurityToken> _signingTokens = null;
static string _issuer = string.Empty;


// SendAsync is used to validate incoming requests contain a valid access token, and sets the current user identity
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
string jwtToken;
string issuer;
List<X509SecurityToken> signingTokens;

if (!TryRetrieveToken(request, out jwtToken))
{
return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}

try
{
// Get tenant information that's used to validate incoming jwt tokens
GetTenantInformation(string.Format("https://login.windows.net/{0}/federationmetadata/2007-06/federationmetadata.xml", domainName), out issuer, out signingTokens);
}
catch (Exception)
{
return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
}

JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler()
{
CertificateValidator = X509CertificateValidator.None
};

TokenValidationParameters validationParameters = new TokenValidationParameters
{
AllowedAudience = audience,
ValidIssuer = issuer,
SigningTokens = signingTokens
};

try
{

// Validate token
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken,
validationParameters);

//set the ClaimsPrincipal on the current thread.
Thread.CurrentPrincipal = claimsPrincipal;

// set the ClaimsPrincipal on HttpContext.Current if the app is running in web hosted environment.
if (HttpContext.Current != null)
{
HttpContext.Current.User = claimsPrincipal;
}

// Verify that required permission is set in the scope claim
if (ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope").Value != "user_impersonation")
{
return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}

return base.SendAsync(request, cancellationToken);
}
catch (SecurityTokenValidationException)
{
return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
catch (Exception)
{
return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
}
}

// Reads the token from the authorization header on the incoming request
private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
{
token = null;
string authzHeader;

if (!request.Headers.Contains("Authorization"))
{
return false;
}

authzHeader = request.Headers.GetValues("Authorization").First<string>();

// Verify Authorization header contains 'Bearer' scheme
token = authzHeader.StartsWith("Bearer ") ? authzHeader.Split('')[1] : null;

if (null == token)
{
return false;
}

return true;
}

/// <summary>
/// Parses the federation metadata document and gets issuer Name and Signing Certificates
/// </summary>
/// <param name="metadataAddress">URL of the Federation Metadata document</param>
/// <param name="issuer">Issuer Name</param>
/// <param name="signingTokens">Signing Certificates in the form of X509SecurityToken</param>
static void GetTenantInformation(string metadataAddress, out string issuer, out List<X509SecurityToken> signingTokens)
{
signingTokens = new List<X509SecurityToken>();

// The issuer and signingTokens are cached for 24 hours. They are updated if any of the conditions in the if condition is true.
if (DateTime.UtcNow.Subtract(_stsMetadataRetrievalTime).TotalHours > 24
|| string.IsNullOrEmpty(_issuer)
|| _signingTokens == null)
{
MetadataSerializer serializer = new MetadataSerializer()
{
CertificateValidationMode = X509CertificateValidationMode.None
};
MetadataBase metadata = serializer.ReadMetadata(XmlReader.Create(metadataAddress));

EntityDescriptor entityDescriptor = (EntityDescriptor)metadata;

// get the issuer name
if (!string.IsNullOrWhiteSpace(entityDescriptor.EntityId.Id))
{
_issuer = entityDescriptor.EntityId.Id;
}

// get the signing certs
_signingTokens = ReadSigningCertsFromMetadata(entityDescriptor);

_stsMetadataRetrievalTime = DateTime.UtcNow;
}

issuer = _issuer;
signingTokens = _signingTokens;
}

static List<X509SecurityToken> ReadSigningCertsFromMetadata(EntityDescriptor entityDescriptor)
{
List<X509SecurityToken> stsSigningTokens = new List<X509SecurityToken>();

SecurityTokenServiceDescriptor stsd = entityDescriptor.RoleDescriptors.OfType<SecurityTokenServiceDescriptor>().First();

if (stsd != null && stsd.Keys != null)
{
IEnumerable<X509RawDataKeyIdentifierClause> x509DataClauses = stsd.Keys.Where(key => key.KeyInfo != null && (key.Use == KeyType.Signing || key.Use == KeyType.Unspecified)).
Select(key => key.KeyInfo.OfType<X509RawDataKeyIdentifierClause>().First());

stsSigningTokens.AddRange(x509DataClauses.Select(clause => new X509SecurityToken(new X509Certificate2(clause.GetX509RawData()))));
}
else
{
throw new InvalidOperationException("There is no RoleDescriptor of type SecurityTokenServiceType in the metadata");
}

return stsSigningTokens;
}
}
}



この状態で再度デバッグ実行し、ブラウザからアクセスするとAuthorizationヘッダがないため、401 Unauthorizedが返ってきます。



とりあえず、今回は WebAPI を作るところまでを紹介しましたので、次回は WAAD で実際に保護するための設定を入れていきます。

Viewing all articles
Browse latest Browse all 770

Trending Articles