Azure AD 多租户 + 身份验证筛选最佳实践
Azure AD Multi-Tenant + Authentication-Filtering Best Practices
我是 C# 和 Azure AD 的新手,所以请向我推荐一些指南和资源,以弄清楚如何最好地只允许我批准的客户列表对我的 MVC 应用程序进行 SSO 和授权。我有一个托管在 Azure VM 上的小型应用程序,并且有一个使用 OpenID Connect 的单租户 Azure AD。我在 Azure AD 中进行了应用注册多租户。我为一些用户设置了一个新的组织帐户,以便在 Azure AD 中进行测试。我读了 this guide,这很好,但在示例中停止了简短的注释
issuer validation is disabled to enable any Azure AD tenant to sign in
我找不到有关如何使租户有资格成为我想要访问我的应用程序的组织的指南。授权来自域 CustomerCompany.com 而非 SomeOtherCompany.com 的用户的最佳方式是什么,即使两者都有 Azure AD 组织帐户?
我不想维护用户列表,因此 Azure AD 协作 B2B 内容不符合要求(上传用户列表和发出邀请)。我希望客户维护自己的用户列表。
我想首先简单地允许来自我的客户的用户并拒绝所有其他用户。
- 有没有一种方法可以在我的组织和 Azure AD 中的其他组织之间建立关系,并以某种方式 allow/deny 在 AAD 登录时使用这些关系对我的应用程序进行授权?
- 是否有来自 Azure AD 的 tenantid 值,我可以将其与我在 SQL 服务器中保留的列表进行比较,以在 AAD 登录后获得 allow/deny 授权?这通常是我必须从客户那里获得的域名或某些 GUID 或其他值吗?
如果客户想要限制他们组织中的某些人使用我的应用程序,最佳做法是:
- 让客户注册我的应用程序或以某种方式将其分配给他们在自己的 Azure AD 中设置的组,以便用户在管理员不允许的情况下不会进行身份验证?或者
- 使用图表 API 和基于该值的 allow/deny 在我的服务器上查询传入用户配置文件的某个值(组成员、部门、经理等)?
如果您能在此处为初学者提供任何操作方法和最佳实践指导,我们将不胜感激。谢谢。
Is there a way I can create a relationship between my org and others in Azure AD and somehow allow/deny authorization to my app using those relationships at the AAD login?
Is there a tenantid value that comes from Azure AD that I can compare to a list I keep in SQL server to allow/deny authorization after AAD login? Is that typically a domain name or some GUID or other value I have to get from the customer?
是的。我们可以编写自定义代码来验证令牌中的 iss
声明,使其符合业务逻辑。以下是使用 OpenIdConnect OWIN 组件的代码供您参考:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
Authority = Authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// we use this notification for injecting our custom logic
SecurityTokenValidated = (context) =>
{
// retriever caller data from the incoming principal
string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
string UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
if (
// the caller comes from an admin-consented, recorded issuer
(db.Tenants.FirstOrDefault(a => ((a.IssValue == issuer) && (a.AdminConsented))) == null)
// the caller is recorded in the db of users who went through the individual onboardoing
&& (db.Users.FirstOrDefault(b =>((b.UPN == UPN) && (b.TenantID == tenantID))) == null)
)
// the caller was neither from a trusted issuer or a registered user - throw to block the authentication flow
throw new SecurityTokenValidationException();
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.OwinContext.Response.Redirect("/Home/Error?message=" + context.Exception.Message);
context.HandleResponse(); // Suppress the exception
return Task.FromResult(0);
}
}
});
并且 here 是一个有用的代码示例供您参考。
Have the customer register my app or somehow assign it to groups they set up in their own Azure AD so the user won't authenticate if not allowed by their admin? OR
Query the incoming user profile at my server for some value (Group Membership, Department, Manager, etc.) using Graph API and allow/deny based on that value?
根据我的理解,应该让定制公司来管理有权访问您的应用程序的用户,因为用户管理是合作伙伴公司的责任。因此,在合作伙伴的公司决定 enable/disable 用户后,您的公司和应用程序不需要制作和额外的工作或更改。
并且要管理可以访问该应用程序的用户,合作伙伴的公司可以使用 Requiring User Assignment 功能。
谢谢。我也在回答我自己的问题,以添加更多关于我如何取得进展的细节(希望它能帮助一些 reader)并提出更详细的问题以了解它的核心。
我现在有办法将传入的经过身份验证的用户的图表数据与我保留的客户公司名称列表进行比较。但这并不理想。这取决于图表 api 获取客户租户的 DisplayName,它应该与我的列表匹配。我担心这可能会改变,然后不允许合法客户。我更愿意使用 Azure AD 中的租户 ID。但我不知道如何在注册时立即获得它。我可以在第一个用户从该租户进行身份验证后得到它,但是我如何在该公司的任何人进行身份验证之前获取客户的租户 ID?我必须要它,还是我可以自己得到它?
是否有更好的方法来过滤对我的 Web 应用程序 (IIS) 的访问以仅允许我的客户?那就是我的详细Q了。
请帮助初学者了解大佬们如何只授权他们的客户访问 IIS Web 应用程序。这是我如何做的开始(在客户过滤部分使用伪代码作弊,因为我想比较像 TenantID(Issuer?)这样的 GUID 而不是像 DisplayName 这样的字符串):
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
public static string custTenantID = "";
//for multi tenant sso, can be 'common' for consumer, 'organization' for work+school, or tenantID for single company
public static readonly string Authority = aadInstance + "common/";
//this is my model for the customer db
public CustomerTenant tenant = new CustomerTenant();
// 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,
//for multi-tenant
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps)
// we inject our own multitenant validation logic
ValidateIssuer = false,
},
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 is invoked after SecurityTokenValidated
//using this Notification to inject logic to authorize only my customers
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext authContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
//look up the customer tenant's DisplayName to compare to my list
Uri servicePointUri = new Uri(graphResourceId);
Uri serviceRoot = new Uri(servicePointUri, custTenantID);
ActiveDirectoryClient client = new ActiveDirectoryClient(serviceRoot, async () => { return await Task.FromResult(result.AccessToken); });
string tenantName = client.TenantDetails.ExecuteAsync().Result.CurrentPage.First().DisplayName;
//pseudo-code: compare this tenantName to db.CustomerTenants.CustomerName and if there is a match then authorize else do not authorize
return Task.FromResult(0);
},
//for multi-tenant sso
RedirectToIdentityProvider = (context) =>
{
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl;
context.ProtocolMessage.PostLogoutRedirectUri = postLogoutRedirectUri;
return Task.FromResult(0);
},
SecurityTokenValidated = (context) =>
{
// retrieve caller data from the incoming principal
string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
string UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
custTenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.OwinContext.Response.Redirect("urlToMyErrorPage?message=" + context.Exception.Message);
context.HandleResponse(); // Suppress the exception
return Task.FromResult(0);
}
}
});
}
我是 C# 和 Azure AD 的新手,所以请向我推荐一些指南和资源,以弄清楚如何最好地只允许我批准的客户列表对我的 MVC 应用程序进行 SSO 和授权。我有一个托管在 Azure VM 上的小型应用程序,并且有一个使用 OpenID Connect 的单租户 Azure AD。我在 Azure AD 中进行了应用注册多租户。我为一些用户设置了一个新的组织帐户,以便在 Azure AD 中进行测试。我读了 this guide,这很好,但在示例中停止了简短的注释
issuer validation is disabled to enable any Azure AD tenant to sign in
我找不到有关如何使租户有资格成为我想要访问我的应用程序的组织的指南。授权来自域 CustomerCompany.com 而非 SomeOtherCompany.com 的用户的最佳方式是什么,即使两者都有 Azure AD 组织帐户?
我不想维护用户列表,因此 Azure AD 协作 B2B 内容不符合要求(上传用户列表和发出邀请)。我希望客户维护自己的用户列表。
我想首先简单地允许来自我的客户的用户并拒绝所有其他用户。
- 有没有一种方法可以在我的组织和 Azure AD 中的其他组织之间建立关系,并以某种方式 allow/deny 在 AAD 登录时使用这些关系对我的应用程序进行授权?
- 是否有来自 Azure AD 的 tenantid 值,我可以将其与我在 SQL 服务器中保留的列表进行比较,以在 AAD 登录后获得 allow/deny 授权?这通常是我必须从客户那里获得的域名或某些 GUID 或其他值吗?
如果客户想要限制他们组织中的某些人使用我的应用程序,最佳做法是:
- 让客户注册我的应用程序或以某种方式将其分配给他们在自己的 Azure AD 中设置的组,以便用户在管理员不允许的情况下不会进行身份验证?或者
- 使用图表 API 和基于该值的 allow/deny 在我的服务器上查询传入用户配置文件的某个值(组成员、部门、经理等)?
如果您能在此处为初学者提供任何操作方法和最佳实践指导,我们将不胜感激。谢谢。
Is there a way I can create a relationship between my org and others in Azure AD and somehow allow/deny authorization to my app using those relationships at the AAD login?
Is there a tenantid value that comes from Azure AD that I can compare to a list I keep in SQL server to allow/deny authorization after AAD login? Is that typically a domain name or some GUID or other value I have to get from the customer?
是的。我们可以编写自定义代码来验证令牌中的 iss
声明,使其符合业务逻辑。以下是使用 OpenIdConnect OWIN 组件的代码供您参考:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
Authority = Authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// we use this notification for injecting our custom logic
SecurityTokenValidated = (context) =>
{
// retriever caller data from the incoming principal
string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
string UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
if (
// the caller comes from an admin-consented, recorded issuer
(db.Tenants.FirstOrDefault(a => ((a.IssValue == issuer) && (a.AdminConsented))) == null)
// the caller is recorded in the db of users who went through the individual onboardoing
&& (db.Users.FirstOrDefault(b =>((b.UPN == UPN) && (b.TenantID == tenantID))) == null)
)
// the caller was neither from a trusted issuer or a registered user - throw to block the authentication flow
throw new SecurityTokenValidationException();
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.OwinContext.Response.Redirect("/Home/Error?message=" + context.Exception.Message);
context.HandleResponse(); // Suppress the exception
return Task.FromResult(0);
}
}
});
并且 here 是一个有用的代码示例供您参考。
Have the customer register my app or somehow assign it to groups they set up in their own Azure AD so the user won't authenticate if not allowed by their admin? OR
Query the incoming user profile at my server for some value (Group Membership, Department, Manager, etc.) using Graph API and allow/deny based on that value?
根据我的理解,应该让定制公司来管理有权访问您的应用程序的用户,因为用户管理是合作伙伴公司的责任。因此,在合作伙伴的公司决定 enable/disable 用户后,您的公司和应用程序不需要制作和额外的工作或更改。
并且要管理可以访问该应用程序的用户,合作伙伴的公司可以使用 Requiring User Assignment 功能。
谢谢。我也在回答我自己的问题,以添加更多关于我如何取得进展的细节(希望它能帮助一些 reader)并提出更详细的问题以了解它的核心。
我现在有办法将传入的经过身份验证的用户的图表数据与我保留的客户公司名称列表进行比较。但这并不理想。这取决于图表 api 获取客户租户的 DisplayName,它应该与我的列表匹配。我担心这可能会改变,然后不允许合法客户。我更愿意使用 Azure AD 中的租户 ID。但我不知道如何在注册时立即获得它。我可以在第一个用户从该租户进行身份验证后得到它,但是我如何在该公司的任何人进行身份验证之前获取客户的租户 ID?我必须要它,还是我可以自己得到它?
是否有更好的方法来过滤对我的 Web 应用程序 (IIS) 的访问以仅允许我的客户?那就是我的详细Q了。
请帮助初学者了解大佬们如何只授权他们的客户访问 IIS Web 应用程序。这是我如何做的开始(在客户过滤部分使用伪代码作弊,因为我想比较像 TenantID(Issuer?)这样的 GUID 而不是像 DisplayName 这样的字符串):
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
public static string custTenantID = "";
//for multi tenant sso, can be 'common' for consumer, 'organization' for work+school, or tenantID for single company
public static readonly string Authority = aadInstance + "common/";
//this is my model for the customer db
public CustomerTenant tenant = new CustomerTenant();
// 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,
//for multi-tenant
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps)
// we inject our own multitenant validation logic
ValidateIssuer = false,
},
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 is invoked after SecurityTokenValidated
//using this Notification to inject logic to authorize only my customers
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext authContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
//look up the customer tenant's DisplayName to compare to my list
Uri servicePointUri = new Uri(graphResourceId);
Uri serviceRoot = new Uri(servicePointUri, custTenantID);
ActiveDirectoryClient client = new ActiveDirectoryClient(serviceRoot, async () => { return await Task.FromResult(result.AccessToken); });
string tenantName = client.TenantDetails.ExecuteAsync().Result.CurrentPage.First().DisplayName;
//pseudo-code: compare this tenantName to db.CustomerTenants.CustomerName and if there is a match then authorize else do not authorize
return Task.FromResult(0);
},
//for multi-tenant sso
RedirectToIdentityProvider = (context) =>
{
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl;
context.ProtocolMessage.PostLogoutRedirectUri = postLogoutRedirectUri;
return Task.FromResult(0);
},
SecurityTokenValidated = (context) =>
{
// retrieve caller data from the incoming principal
string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
string UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
custTenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.OwinContext.Response.Redirect("urlToMyErrorPage?message=" + context.Exception.Message);
context.HandleResponse(); // Suppress the exception
return Task.FromResult(0);
}
}
});
}