(一部、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 を設定しておかなくてもユーザの代理として機能する、というところです。
◆作業の流れ
作業の流れは以下の通りです。
- WebAPI の作成:今回は ASP.NET MVC4 の WebAPI を使います
- WebAPI の保護:WAAD を使って認可する様に設定を行います
- WebAPI を WAAD に登録:WAAD の保護対象リソースとして WAAD へ登録します
- OAuth クライアントの作成:本来は真面目にクライアントも作るのですが、今回は生の動きを見るために Chrome Extension の Advanced REST Client とダミー URL を使います
- OAuth クライアントを WAAD に登録:WAAD を使うアプリケーションとして OAuth クライアントを登録します
- WebAPI へのアクセス許可設定:OAuth クライアントが WebAPI へアクセスできるように設定します
- 動作確認:実際に 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 で実際に保護するための設定を入れていきます。
0 件のコメント:
コメントを投稿