將 OWIN 與 Microsoft.Identity.Web 整合。

使用 Microsoft.Identity.Web.OWIN 套件,為在 .NET Framework 4.7.2+ 上的 ASP.NET MVC 和 Web API 應用程式加入現代認證功能。

了解 OWIN 整合

Microsoft.Identity.Web.OWIN 套件將 Microsoft.Identity.Web 的功能帶給使用 OWIN 中介軟體和 Microsoft Entra ID 的 ASP.NET MVC 和 Web API 應用程式。

回顧主要效益

下表總結了該套件的主要特色與優勢。

Feature 優點
代幣收購者工廠 自動憑證擷取與快取
控制器擴充功能 輕鬆取得 GraphServiceClient 和 IDownstreamApi
分散式令牌快取 內建支援 SQL Server、Redis、Cosmos DB、PostgreSQL
自動令牌更新 透明地處理令牌更新
漸進同意 無縫的同意流程整合

回顧支援情境

Microsoft。Identity.Web.OWIN 支援以下應用程式類型與情境。

  • ASP.NET MVC Web Applications (.NET Framework 4.7.2+)
  • ASP.NET Web API (.NET Framework 4.7.2+)
  • 混合應用程式 (MVC + Web API)
  • 從控制器呼叫 Microsoft Graph
  • 呼叫具有自動認證的下游 API

安裝套件

安裝 Microsoft.Identity.Web.OWIN NuGet 套件,使用您偏好的方法。

請在 封裝管理員 Console 執行以下指令:

Install-Package Microsoft.Identity.Web.OWIN

或者,也可以執行以下指令,搭配 .NET CLI:

dotnet add package Microsoft.Identity.Web.OWIN

依賴關係自動包含:

  • Microsoft.Identity.Web.TokenAcquisition
  • Microsoft.Identity.Web.TokenCache
  • Microsoft.Owin
  • System.Web

設定應用程式

請設定您的應用程式設定,讓它連接到 Microsoft Entra 及下游 API。

設定 Web.config

將以下Microsoft Entra及下游 API 設定加入你的 Web.config 檔案。

<configuration>
  <appSettings>
    <!-- Microsoft Entra ID Configuration -->
    <add key="AzureAd:Instance" value="https://login.microsoftonline.com/" />
    <add key="AzureAd:TenantId" value="your-tenant-id" />
    <add key="AzureAd:ClientId" value="your-client-id" />
    <add key="AzureAd:ClientSecret" value="your-client-secret" />
    <add key="AzureAd:RedirectUri" value="https://localhost:44368/" />
    <add key="AzureAd:PostLogoutRedirectUri" value="https://localhost:44368/" />

    <!-- Microsoft Graph Configuration -->
    <add key="DownstreamApi:MicrosoftGraph:BaseUrl" value="https://graph.microsoft.com/v1.0" />
    <add key="DownstreamApi:MicrosoftGraph:Scopes" value="user.read" />

    <!-- Custom Downstream API Configuration -->
    <add key="DownstreamApi:TodoListService:BaseUrl" value="https://localhost:44351" />
    <add key="DownstreamApi:TodoListService:Scopes" value="api://todo-api-client-id/.default" />
  </appSettings>

  <connectionStrings>
    <!-- Optional: SQL Server Token Cache -->
    <add name="TokenCache"
         connectionString="Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=TokenCache;Integrated Security=True;" />
  </connectionStrings>
</configuration>

配置 appsettings.json(替代方案)

你也可以將設定存放在 appsettings.json 檔案中,如以下範例所示。

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-client-id",
    "ClientSecret": "your-client-secret",
    "RedirectUri": "https://localhost:44368/",
    "PostLogoutRedirectUri": "https://localhost:44368/"
  },
  "DownstreamApi": {
    "MicrosoftGraph": {
      "BaseUrl": "https://graph.microsoft.com/v1.0",
      "Scopes": "user.read"
    },
    "TodoListService": {
      "BaseUrl": "https://localhost:44351",
      "Scopes": "api://todo-api-client-id/.default"
    }
  }
}

設立新創班

在你的新創類別中註冊認證中介軟體、權杖取得及下游 API 服務。

設定 App_Start/Startup.Auth.cs

以下程式碼展示了 Microsoft.Identity.Web.OWIN 的完整設定:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.OWIN;
using Microsoft.Identity.Web.TokenCacheProviders.Distributed;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Configuration;
using System.Web;

namespace MyMvcApp
{
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            // Set default authentication type
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            // Configure cookie authentication
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                CookieName = "MyApp.Auth",
                ExpireTimeSpan = TimeSpan.FromHours(1),
                SlidingExpiration = true
            });

            // Configure OpenID Connect authentication
            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    ClientId = ConfigurationManager.AppSettings["AzureAd:ClientId"],
                    Authority = $"https://login.microsoftonline.com/{ConfigurationManager.AppSettings["AzureAd:TenantId"]}",
                    RedirectUri = ConfigurationManager.AppSettings["AzureAd:RedirectUri"],
                    PostLogoutRedirectUri = ConfigurationManager.AppSettings["AzureAd:PostLogoutRedirectUri"],

                    Scope = "openid profile email offline_access",
                    ResponseType = "code id_token",

                    TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        NameClaimType = "preferred_username"
                    },

                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        AuthenticationFailed = context =>
                        {
                            context.HandleResponse();
                            context.Response.Redirect("/Error?message=" + context.Exception.Message);
                            return Task.FromResult(0);
                        }
                    }
                });

            // Configure Microsoft Identity Web services
            var services = CreateOwinServiceCollection();

            // Add token acquisition
            services.AddTokenAcquisition();

            // Add Microsoft Graph support
            services.AddMicrosoftGraph();

            // Add downstream API support
            services.AddDownstreamApi("MicrosoftGraph", services.BuildServiceProvider()
                .GetRequiredService<IConfiguration>().GetSection("DownstreamApi:MicrosoftGraph"));

            services.AddDownstreamApi("TodoListService", services.BuildServiceProvider()
                .GetRequiredService<IConfiguration>().GetSection("DownstreamApi:TodoListService"));

            // Configure token cache (choose one option)
            ConfigureTokenCache(services);

            // Build service provider
            var serviceProvider = services.BuildServiceProvider();

            // Create and register token acquirer factory
            var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
            tokenAcquirerFactory.Build(serviceProvider);

            // Add OWIN token acquisition middleware
            app.Use<OwinTokenAcquisitionMiddleware>(tokenAcquirerFactory);
        }

        private IServiceCollection CreateOwinServiceCollection()
        {
            var services = new ServiceCollection();

            // Add configuration from appsettings.json and/or Web.config
            IConfiguration configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", optional: true)
                .AddInMemoryCollection(new Dictionary<string, string>
                {
                    ["AzureAd:Instance"] = ConfigurationManager.AppSettings["AzureAd:Instance"],
                    ["AzureAd:TenantId"] = ConfigurationManager.AppSettings["AzureAd:TenantId"],
                    ["AzureAd:ClientId"] = ConfigurationManager.AppSettings["AzureAd:ClientId"],
                    ["AzureAd:ClientSecret"] = ConfigurationManager.AppSettings["AzureAd:ClientSecret"],
                    ["DownstreamApi:MicrosoftGraph:BaseUrl"] = ConfigurationManager.AppSettings["DownstreamApi:MicrosoftGraph:BaseUrl"],
                    ["DownstreamApi:MicrosoftGraph:Scopes"] = ConfigurationManager.AppSettings["DownstreamApi:MicrosoftGraph:Scopes"],
                })
                .Build();

            services.AddSingleton(configuration);

            return services;
        }

        private void ConfigureTokenCache(IServiceCollection services)
        {
            // Option 1: In-memory cache (development)
            services.AddDistributedTokenCaches(cacheServices =>
            {
                cacheServices.AddDistributedMemoryCache();
            });

            // Option 2: SQL Server cache (production)
            /*
            services.AddDistributedTokenCaches(cacheServices =>
            {
                cacheServices.AddDistributedSqlServerCache(options =>
                {
                    options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString;
                    options.SchemaName = "dbo";
                    options.TableName = "TokenCache";
                    options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
                });
            });
            */

            // Option 3: Redis cache (production, high-scale)
            /*
            services.AddDistributedTokenCaches(cacheServices =>
            {
                cacheServices.AddStackExchangeRedisCache(options =>
                {
                    options.Configuration = ConfigurationManager.AppSettings["Redis:ConnectionString"];
                    options.InstanceName = "MyMvcApp_";
                });
            });
            */
        }
    }
}

整合控制器

透過套件提供的擴充方法,從控制器存取 Microsoft Graph 及下游 API。

整合 MVC 控制器

以下範例說明如何使用控制器擴充方法來存取 Microsoft Graph:

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.OWIN;
using Microsoft.Graph;
using System.Threading.Tasks;
using System.Web.Mvc;

namespace MyMvcApp.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {
        // GET: Home/Index
        public async Task<ActionResult> Index()
        {
            try
            {
                // Access Microsoft Graph using extension method
                var graphClient = this.GetGraphServiceClient();
                var user = await graphClient.Me.GetAsync();

                ViewBag.UserName = user.DisplayName;
                ViewBag.Email = user.Mail ?? user.UserPrincipalName;
                ViewBag.JobTitle = user.JobTitle;

                return View();
            }
            catch (MsalUiRequiredException)
            {
                // Incremental consent required
                return new ChallengeResult();
            }
            catch (Exception ex)
            {
                return View("Error", new ErrorViewModel { Message = ex.Message });
            }
        }

        // GET: Home/Profile
        public async Task<ActionResult> Profile()
        {
            var graphClient = this.GetGraphServiceClient();

            // Get user profile
            var user = await graphClient.Me
                .GetAsync(requestConfig => requestConfig.QueryParameters.Select = new[] { "displayName", "mail", "jobTitle", "department" });

            return View(user);
        }

        // GET: Home/Photo
        public async Task<ActionResult> Photo()
        {
            var graphClient = this.GetGraphServiceClient();

            try
            {
                // Get user photo
                var photoStream = await graphClient.Me.Photo.Content.GetAsync();
                return File(photoStream, "image/jpeg");
            }
            catch (ServiceException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                return File(Server.MapPath("~/Content/images/default-user.png"), "image/png");
            }
        }
    }
}

整合 Web API 控制器

以下範例展示了如何使用 ApiController 擴充方法呼叫下游 API:

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.OWIN;
using Microsoft.Identity.Abstractions;
using System.Threading.Tasks;
using System.Web.Http;

namespace MyWebApi.Controllers
{
    [Authorize]
    [RoutePrefix("api/todos")]
    public class TodoController : ApiController
    {
        // GET: api/todos
        [HttpGet]
        [Route("")]
        public async Task<IHttpActionResult> GetTodos()
        {
            try
            {
                // Call downstream API using extension method
                var downstreamApi = this.GetDownstreamApi();

                var todos = await downstreamApi.GetForUserAsync<List<TodoItem>>(
                    "TodoListService",
                    options =>
                    {
                        options.RelativePath = "api/todolist";
                    });

                return Ok(todos);
            }
            catch (MsalUiRequiredException)
            {
                return Unauthorized();
            }
            catch (HttpRequestException ex)
            {
                return InternalServerError(ex);
            }
        }

        // POST: api/todos
        [HttpPost]
        [Route("")]
        public async Task<IHttpActionResult> CreateTodo([FromBody] TodoItem todo)
        {
            var downstreamApi = this.GetDownstreamApi();

            var createdTodo = await downstreamApi.PostForUserAsync<TodoItem, TodoItem>(
                "TodoListService",
                todo,
                options =>
                {
                    options.RelativePath = "api/todolist";
                });

            return Created($"api/todos/{createdTodo.Id}", createdTodo);
        }
    }
}

呼叫 Microsoft Graph

使用 GraphServiceClient 來與控制器Microsoft Graph資料互動。

設定 Microsoft Graph 用戶端

Microsoft Graph用戶端已在 Startup.Auth.cs 設定,呼叫如下:

services.AddMicrosoftGraph();

在控制器中使用 GraphServiceClient

以下範例展示了 MVC 控制器中常見的 Microsoft Graph 操作。

[Authorize]
public class GraphController : Controller
{
    public async Task<ActionResult> MyProfile()
    {
        var graphClient = this.GetGraphServiceClient();
        var user = await graphClient.Me.GetAsync();

        return View(user);
    }

    public async Task<ActionResult> MyManager()
    {
        var graphClient = this.GetGraphServiceClient();
        var manager = await graphClient.Me.Manager.GetAsync();

        return View(manager);
    }

    public async Task<ActionResult> MyDirectReports()
    {
        var graphClient = this.GetGraphServiceClient();
        var directReports = await graphClient.Me.DirectReports.GetAsync();

        return View(directReports.Value);
    }

    public async Task<ActionResult> SendEmail([FromBody] EmailMessage message)
    {
        var graphClient = this.GetGraphServiceClient();

        var email = new Message
        {
            Subject = message.Subject,
            Body = new ItemBody
            {
                ContentType = BodyType.Text,
                Content = message.Body
            },
            ToRecipients = new[]
            {
                new Recipient
                {
                    EmailAddress = new EmailAddress
                    {
                        Address = message.To
                    }
                }
            }
        };

        await graphClient.Me.SendMail.PostAsync(new SendMailPostRequestBody
        {
            Message = email
        });

        return RedirectToAction("Index");
    }
}

呼叫下游 API

註冊並呼叫下游 API,並自動從控制器取得令牌。

設定下游 API

用以下程式碼在 Startup.Auth.cs 中註冊下游 API:

services.AddDownstreamApi("TodoListService", configuration.GetSection("DownstreamApi:TodoListService"));

將相應設定加入 Web.config。

<add key="DownstreamApi:TodoListService:BaseUrl" value="https://localhost:44351" />
<add key="DownstreamApi:TodoListService:Scopes" value="api://todo-api-client-id/.default" />

在控制器中使用 IDownstreamApi

以下範例展示了如何針對 MVC 控制器的下游 API 執行 CRUD 操作。

[Authorize]
public class TodoController : Controller
{
    // GET all todos
    public async Task<ActionResult> Index()
    {
        var downstreamApi = this.GetDownstreamApi();

        var todos = await downstreamApi.GetForUserAsync<List<TodoItem>>(
            "TodoListService",
            options =>
            {
                options.RelativePath = "api/todolist";
            });

        return View(todos);
    }

    // GET specific todo
    public async Task<ActionResult> Details(int id)
    {
        var downstreamApi = this.GetDownstreamApi();

        var todo = await downstreamApi.GetForUserAsync<TodoItem>(
            "TodoListService",
            options =>
            {
                options.RelativePath = $"api/todolist/{id}";
            });

        return View(todo);
    }

    // POST new todo
    [HttpPost]
    public async Task<ActionResult> Create(TodoItem todo)
    {
        var downstreamApi = this.GetDownstreamApi();

        var createdTodo = await downstreamApi.PostForUserAsync<TodoItem, TodoItem>(
            "TodoListService",
            todo,
            options =>
            {
                options.RelativePath = "api/todolist";
            });

        return RedirectToAction("Index");
    }

    // PUT update todo
    [HttpPost]
    public async Task<ActionResult> Edit(int id, TodoItem todo)
    {
        var downstreamApi = this.GetDownstreamApi();

        await downstreamApi.CallApiForUserAsync(
            "TodoListService",
            options =>
            {
                options.HttpMethod = HttpMethod.Put;
                options.RelativePath = $"api/todolist/{id}";
                options.RequestBody = todo;
            });

        return RedirectToAction("Index");
    }

    // DELETE todo
    [HttpPost]
    public async Task<ActionResult> Delete(int id)
    {
        var downstreamApi = this.GetDownstreamApi();

        await downstreamApi.CallApiForUserAsync(
            "TodoListService",
            options =>
            {
                options.HttpMethod = HttpMethod.Delete;
                options.RelativePath = $"api/todolist/{id}";
            });

        return RedirectToAction("Index");
    }
}

探索範例應用

請參考以下範例來看 Microsoft.Identity.Web.OWIN 的實際應用。

檢視官方 Microsoft 範例

下表列出展示 Microsoft.Identity.Web.OWIN 整合的官方範例。

範例 說明
ms-identity-aspnet-webapp-openidconnect ASP.NET MVC 應用程式,與 Microsoft 合作。Identity.Web.OWIN
關鍵檔案 App_Start/Startup.Auth.cs、Controllers/HomeController.cs

複製並執行以下命令的範例:

git clone https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect
cd ms-identity-aspnet-webapp-openidconnect
# Update Web.config with your Microsoft Entra app registration
# Run in Visual Studio

遵循最佳做法

應用這些建議的模式,並在使用 Microsoft.Identity.Web.OWIN 時避免常見的錯誤。

1. 在生產環境中使用分散式快取:

//  Production
services.AddDistributedTokenCaches(cacheServices =>
{
    cacheServices.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString;
        options.SchemaName = "dbo";
        options.TableName = "TokenCache";
        options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
    });
});

2. 優雅地處理漸進式同意:

try
{
    var graphClient = this.GetGraphServiceClient();
    var user = await graphClient.Me.GetAsync();
}
catch (MsalUiRequiredException)
{
    // User needs to consent to additional scopes
    return new ChallengeResult();
}

3. 使用相關 ID 進行故障排除:

var downstreamApi = this.GetDownstreamApi();
var correlationId = Guid.NewGuid();

var result = await downstreamApi.GetForUserAsync<Todo>(
    "TodoListService",
    options =>
    {
        options.RelativePath = $"api/todolist/{id}";
        options.TokenAcquisitionOptions = new TokenAcquisitionOptions
        {
            CorrelationId = correlationId
        };
    });

4. 實施正確的錯誤處理:

try
{
    // Call API
}
catch (MsalUiRequiredException)
{
    return new ChallengeResult();
}
catch (HttpRequestException ex)
{
    logger.Error($"API call failed: {ex.Message}");
    return View("Error");
}

避免常見錯誤

1. 不要在網路農場中使用記憶體快取:

//  Wrong for load-balanced scenarios
services.AddDistributedTokenCaches(cacheServices =>
{
    cacheServices.AddDistributedMemoryCache();
});

//  Correct
services.AddDistributedTokenCaches(cacheServices =>
{
    cacheServices.AddDistributedSqlServerCache(/* ... */);
});

2. 不要硬編碼設定:

//  Wrong
ClientId = "your-client-id-here"

//  Correct
ClientId = ConfigurationManager.AppSettings["AzureAd:ClientId"]

3. 不要忽略代幣到期:

//  Microsoft.Identity.Web.OWIN handles this automatically
// No manual token refresh needed!

排除常見問題

檢視以下針對設定與執行時常見問題的解決方案。

解決常見問題

問題 1:「找不到 IAuthorizationHeaderProvider」

解決方案: 確保 OwinTokenAcquirerFactory 註冊於 Startup.Auth.cs:

var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
tokenAcquirerFactory.Build(serviceProvider);
app.Use<OwinTokenAcquisitionMiddleware>(tokenAcquirerFactory);

問題 2:「找不到 GraphServiceClient」

解決方案:在AddMicrosoftGraph()中添加Startup.Auth.cs:

services.AddMicrosoftGraph();

問題 3:令牌快取無法持久

解決方案: 驗證分散式快取配置:

services.AddDistributedTokenCaches(cacheServices =>
{
    cacheServices.AddDistributedSqlServerCache(options =>
    {
        // Ensure connection string is correct
        options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString;
    });
});

了解更多相關功能與情境。