c# - 使用 Azure AD 登录后使用 WebApp 静默获取令牌失败?
问题描述
我将 WebApp 和 WebAPI 注册到同一个 Azure AD 中。
我正在尝试从 WebApp 调用 WebAPI。
我已在 azure AD Applcaition 中将服务 WebAPI 添加到我的 WebApp 中。像下面 -
当我运行 WebAPI 时,它会在登录成功后给我登录屏幕,我可以访问 WebAPI 方法。这是正常行为。
当我运行 WebApp 时,它会显示相同的登录屏幕,登录成功后我可以看到 WebApp。
现在我想从 WebApp 调用 WebAPI 方法,但我不想要 WebAPI 的登录屏幕,因为当我运行 WebApp 时,我将获得登录屏幕,登录后我希望通过使用相同的用户,我应该能够访问 WebAPI 而无需再次登录,因为我有适用于 WebApp 和 WebAPI 的令牌。
WebAPI 代码 -
启动.Auth.cs
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
private static string aadInstance = EnsureTrailingSlash(ConfigurationManager.AppSettings["ida:AADInstance"]);
private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
public static readonly string Authority = aadInstance + tenantId;
// This is the resource ID of the AAD Graph API. We'll need this to request a token to call the Graph API.
string graphResourceId = "https://graph.windows.net";
public void ConfigureAuth(IAppBuilder app)
{
ApplicationDbContext db = new ApplicationDbContext();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCodeAsync(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId).Result;
return Task.FromResult(0);
}
}
});
}
private static string EnsureTrailingSlash(string value)
{
if (value == null)
{
value = string.Empty;
}
if (!value.EndsWith("/", StringComparison.Ordinal))
{
return value + "/";
}
return value;
}
}
测试控制器.cs
[Authorize]
public class TestController : ApiController
{
[HttpGet]
[Route("api/getdata")]
public IEnumerable<string> GetData()
{
return new string[] { "value1", "value2" };
}
}
网络应用程序代码 -
Startup.Auth.cs
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
private static string aadInstance = EnsureTrailingSlash(ConfigurationManager.AppSettings["ida:AADInstance"]);
private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
public static readonly string Authority = aadInstance + tenantId;
// This is the resource ID of the AAD Graph API. We'll need this to request a token to call the Graph API.
string graphResourceId = "https://graph.windows.net";
public void ConfigureAuth(IAppBuilder app)
{
ApplicationDbContext db = new ApplicationDbContext();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCodeAsync(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId).Result;
return Task.FromResult(0);
}
}
});
}
private static string EnsureTrailingSlash(string value)
{
if (value == null)
{
value = string.Empty;
}
if (!value.EndsWith("/", StringComparison.Ordinal))
{
return value + "/";
}
return value;
}
}
家庭控制器.cs
[Authorize]
public class HomeController : Controller
{
private static string clientIdWebApp = ConfigurationManager.AppSettings["ida:clientIdWebApp"];
private static string clientIdWebApi = ConfigurationManager.AppSettings["ida:clientIdWebApi"];
private static string clientSecretWebApp = ConfigurationManager.AppSettings["ida:clientSecretWebApp"];
private static string aadInstance = (ConfigurationManager.AppSettings["ida:AADInstance"]);
private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
private static string PostLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
Uri redirectUri = new Uri(PostLogoutRedirectUri);
public static readonly string Authority = aadInstance + tenantId;
public ActionResult Index()
{
return View();
}
public async System.Threading.Tasks.Task<ActionResult> About()
{
ViewBag.Message = "Your application description page.";
try
{
AuthenticationResult result = null;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Startup.Authority, new ADALTokenCache(userObjectID));
ClientCredential credential = new ClientCredential(clientIdWebApp, clientSecretWebApp);
//AcquireTokenSilentAsync should have to work as i'm accessing WebAPI using same user I logged in to WebApp
result = authContext.AcquireTokenSilentAsync(clientIdWebApi,credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId)).Result;
// gettign exception {"Failed to acquire token silently as no token was found in the cache. Call method AcquireToken"} but I got match id into cache.
// and if use AcquireToken instead then it works but api response is login html //page instead of api output
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://MYWEBAPI/api/getdata");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
// Return the user's profile in the view.
if (response.IsSuccessStatusCode)
{
string responseString = await response.Content.ReadAsStringAsync();
}
}
catch (AdalException ex)
{
}
return View();
}
}
AdalTokenCache.cs - WebApp 和 WebAPI 相同
public class ADALTokenCache : TokenCache
{
private ApplicationDbContext db = new ApplicationDbContext();
private string userId;
private UserTokenCache Cache;
public ADALTokenCache(string signedInUserId)
{
// associate the cache to the current user of the web app
userId = signedInUserId;
this.AfterAccess = AfterAccessNotification;
this.BeforeAccess = BeforeAccessNotification;
this.BeforeWrite = BeforeWriteNotification;
// look up the entry in the database
Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
// place the entry in memory
this.Deserialize((Cache == null) ? null : MachineKey.Unprotect(Cache.cacheBits,"ADALCache"));
}
// clean up the database
public override void Clear()
{
base.Clear();
var cacheEntry = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
db.UserTokenCacheList.Remove(cacheEntry);
db.SaveChanges();
}
// Notification raised before ADAL accesses the cache.
// This is your chance to update the in-memory copy from the DB, if the in-memory version is stale
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
if (Cache == null)
{
// first time access
Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
}
else
{
// retrieve last write from the DB
var status = from e in db.UserTokenCacheList
where (e.webUserUniqueId == userId)
select new
{
LastWrite = e.LastWrite
};
// if the in-memory copy is older than the persistent copy
if (status.First().LastWrite > Cache.LastWrite)
{
// read from from storage, update in-memory copy
Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
}
}
this.Deserialize((Cache == null) ? null : MachineKey.Unprotect(Cache.cacheBits, "ADALCache"));
}
// Notification raised after ADAL accessed the cache.
// If the HasStateChanged flag is set, ADAL changed the content of the cache
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if state changed
if (this.HasStateChanged)
{
if (Cache == null)
{
Cache = new UserTokenCache
{
webUserUniqueId = userId
};
}
Cache.cacheBits = MachineKey.Protect(this.Serialize(), "ADALCache");
Cache.LastWrite = DateTime.Now;
// update the DB and the lastwrite
db.Entry(Cache).State = Cache.UserTokenCacheId == 0 ? EntityState.Added : EntityState.Modified;
db.SaveChanges();
this.HasStateChanged = false;
}
}
void BeforeWriteNotification(TokenCacheNotificationArgs args)
{
// if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry
}
public override void DeleteItem(TokenCacheItem item)
{
base.DeleteItem(item);
}
}
最重要的是,我发现 webapi 也具有AccountController
以下相同的登录代码,就像 webapp 一样。在这种情况下应该怎么做?
public class AccountController : BaseMvcController
{
public void SignIn()
{
// Send an OpenID Connect sign-in request.
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
public void SignOut()
{
string callbackUrl = Url.Action("SignOutCallback", "Account", routeValues: null, protocol: Request.Url.Scheme);
HttpContext.GetOwinContext().Authentication.SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
}
public ActionResult SignOutCallback()
{
if (Request.IsAuthenticated)
{
// Redirect to home page if the user is authenticated.
return RedirectToAction("Index", "Home");
}
return View();
}
}
解决方案
AcquireTokenSilentAsync
仅当您的应用程序至少之前已经为目标资源(在您的情况下为后端 Web API)获取了一个有效令牌并且将该令牌缓存以供后续使用时,该方法才能为您提供帮助。
您可能会收到此错误,因为您甚至没有真正通过 Web API 的身份验证一次(即为 Web API 获取了一个有效的令牌并传递了一次),因此缓存中没有可用的内容。
简而言之,您将无法在第一次使用 AcquireTokenSilentAsync 进行身份验证。
为了进一步理解,请查看您在问题本身中共享的 GitHub 示例。保护后端 Web API
示例代码首先使用授权代码流获取 Web API 的有效令牌。
只有当第一个有效令牌存在时,它才会被缓存,随后的调用可以由
authContext.AcquireTokenSilentAsync
. 这也作为示例文档的一部分明确说明。
资源ID。在 Azure AD 中注册 Web API 时创建的 Web API 的 App ID URI
令牌缓存。缓存访问令牌的对象。请参阅令牌缓存。
如果 AcquireTokenByAuthorizationCodeAsync 成功,ADAL 会缓存令牌。稍后,您可以通过调用 AcquireTokenSilentAsync 从缓存中获取令牌
示例代码
首次使用授权码流获取有效令牌
// OpenID Connect 中间件在获得授权码时发送此事件。
public override async Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context) { string authorizationCode = context.ProtocolMessage.Code; string authority = "https://login.microsoftonline.com/" + tenantID string resourceID = "https://tailspin.onmicrosoft.com/surveys.webapi" // App ID URI ClientCredential credential = new ClientCredential(clientId, clientSecret); AuthenticationContext authContext = new AuthenticationContext(authority, tokenCache); AuthenticationResult authResult = await authContext.AcquireTokenByAuthorizationCodeAsync( authorizationCode, new Uri(redirectUri), credential, resourceID); // If successful, the token is in authResult.AccessToken }
稍后,您可以通过调用 AcquireTokenSilentAsync 从缓存中获取令牌:
AuthenticationContext authContext = new AuthenticationContext(authority, tokenCache); var result = await authContext.AcquireTokenSilentAsync(resourceID, credential, new UserIdentifier(userId, UserIdentifierType.UniqueId));
推荐阅读
- python-3.x - Beautifulsoup 没有解析 html 页面中的所有链接
- electron - 如何在 Atom 编辑器中禁用鼠标中键粘贴?
- php - Laravel 弹性搜索不提供包含或类似匹配
- java - 只有在三星手机图像从图库上传时才会旋转
- node.js - HTTP 基本身份验证在使用 node.js 的 Safari 浏览器中不起作用?
- html - 从可访问性的角度来看,我对块引用元素的使用是否可以接受?
- angular - Angular 7 测试:更改 Cypress.io 中的材料单选按钮选项
- javascript - Vue.js 2 + Webpack,V-Model 从 router.params 绑定和加载数据,可选默认值不起作用
- java - 如何使用 ftpclient java 从 ftp 下载前 20 个文件
- javascript - 绘制并拖动线条的边缘