如何pass/verify 在.net core web app 和web api 之间打开ID token?
How to pass/verify Open ID token between .net core web app and web api?
我们有以下架构:
- .NET 核心 3.1 Web 应用程序使用剃刀页面、jQuery 等作为 UI(不是 angular 应用程序)
- .NET core 3.1 web api 应用程序作为我们的 api 层
- Okta 作为我们的身份提供者
我已经在 Web 应用程序中实现了 Okta 小部件和中间件。用户可以登录,之后我就可以获得 ClaimsPrincipal,访问他们的所有范围,并获取我通过 open id 存储的任何自定义配置文件数据。我可以通过 [Authorize] 装饰获得视图。一切正常。
我现在需要做的是在 API 端实施安全检查。我花了很多时间查看示例并发现了很多,但我要么遗漏了一些明显的东西,要么我所做的是独一无二的(我无法想象我所做的是那么独特)。基本上我需要做的是:
- 让网络应用程序将身份验证和 ID 令牌传递给 api
- 让 api 能够验证令牌,然后从 id 令牌中解密用户信息
这样我就可以在 API 端实施必要的安全逻辑。假设 return 的客户订单是 API - 我需要确保调用它的用户是管理员或实际客户(所以我不 return客户数据给不应该看到的人)。我已经弄清楚了所有角色的东西,我就是不能,为了我的生活,弄清楚如何通过令牌确定某人是谁?
传递令牌非常简单,但如何从 ClaimsPrincipal 对象中获取令牌?或者我是否需要在用户登录后调用 Okta API 来专门获取访问和 ID 令牌?
当然,我必须弄清楚如何让 API 端正确验证和解析发送的令牌。
如果任何人都可以帮助我开始这个或为我指出正确的方向作为例子,我将非常感激。至此,我已经阅读了我能找到的关于 Owin、OpenID、Okta、.net core 授权的所有文章。
您的 ID 提供者,在本例中为 Okta,将颁发一个 OpenID Connect 授权持有者令牌,您需要将该令牌传递给您想要保护的任何应用程序。
在应用程序的 Web Api 端,您需要注册中间件来处理 Okta 的 OpenID Connect 令牌。然后你可以用 [Authorize]
装饰你的 controllers/actions 并且你可以检查身份声明。
多亏了 Cameron Tinker 的建议,我才得以完成这项工作。有几件事让我很困惑,所以我会在这里分享它们,以防有人有同样的经历。
如果您使用的是 Okta,则可以通过 Okta 中间件包完成所有这些操作。您可以只使用 c# OpenID 库来完成,但 Okta.AspNetCore 库会有所帮助。
首先在网络应用中注册中间件。 Okta 在他们的网站上有很多这样的例子,而且非常简单。
在您的网络应用程序中,您可以使用它来获取令牌(当然是在用户通过身份验证之后)
await context.HttpContext?.GetTokenAsync("id_token")
通过标准机制将其作为 header 的一部分在您的 API 调用中发送:
"Authorization" : "Bearer [token]"
在 Web API 端,您使用相同的 Okta.AspNetCore 中间件包,然后可以使用 [Authorize] 装饰您的控制器以对它们强制执行身份验证。这是我被绊倒的地方。如果您没有使用 Okta 中的默认身份验证服务器并为您的应用程序设置了自定义服务器,则需要在配置中指定它和受众:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OktaDefaults.ApiAuthenticationScheme;
options.DefaultChallengeScheme = OktaDefaults.ApiAuthenticationScheme;
options.DefaultSignInScheme = OktaDefaults.ApiAuthenticationScheme;
})
.AddOktaWebApi(new OktaWebApiOptions()
{
OktaDomain = oktaDomain,
AuthorizationServerId = authServerId,
Audience = clientId
});
services.AddAuthorization();
我完全忘记了观众部分——根据令牌验证的工作方式,这部分是必需的。
从那里开始,中间件负责为您填充 ClaimsPrincipal,因此您可以通过 ClaimsPrincipal (HttpContext.User) 访问用户信息。我最终创建了一个 "CurrentUserService" 并将其拉出到它自己的库中,这样我就可以在那里整合我所有的身份验证处理程序;从而允许我的网络应用程序和网络 api 代码以相同的方式检查权限并检索有关当前用户的信息。如果您有兴趣,代码在这里:
public interface ICurrentUserService
{
public ClaimsPrincipal GetCurrentUser();
public string GetCurrentUserDisplayName();
public string GetCurrentUserFullName();
public string GetCurrentUserId();
public DateTime? GetCurrentUserDob();
public string GetCurrentUserGender();
public AddressFromClaimsDTO GetCurentUserAddress();
public bool IsAuthenticated();
}
public class CurrentUserService : ICurrentUserService
{
private const string FULL_ADDRESS_CLAIM_TYPE = "address";
private readonly IHttpContextAccessor _context;
public CurrentUserService(IHttpContextAccessor context)
{
_context = context;
}
/// <summary>
/// Gets whether or not the current user context is authenticated.
/// </summary>
/// <returns></returns>
public bool IsAuthenticated()
{
return GetCurrentUser().Identity.IsAuthenticated;
}
/// <summary>
/// Gets the current user's address.
/// TODO: tie this into our address data model... but if addresses live in Okta what does that mean?
/// </summary>
/// <returns></returns>
public AddressFromClaimsDTO GetCurentUserAddress()
{
var addressClaim = GetClaim(FULL_ADDRESS_CLAIM_TYPE);
if (addressClaim != null)
{
//var parseValue = addressClaim.Value.ToString().Replace("{address:", "{\"address\":");
var address = JsonSerializer.Deserialize<AddressFromClaimsDTO>(addressClaim.Value.ToString());
return address;
}
else
{
return new AddressFromClaimsDTO();
}
}
public ClaimsPrincipal GetCurrentUser()
{
return _context.HttpContext.User;
}
public string GetCurrentUserDisplayName()
{
return GetCurrentUser().Identity.Name;
}
public string GetCurrentUserFullName()
{
throw new NotImplementedException();
}
public string GetCurrentUserId()
{
throw new NotImplementedException();
}
public DateTime? GetCurrentUserDob()
{
var claim = GetClaim("birthdate");
if (claim != null && !string.IsNullOrEmpty(claim.Value))
{
return DateTime.Parse(claim.Value);
}
else
{
return null;
}
}
public string GetCurrentUserGender()
{
return GetClaim("gender")?.Value.ToString();
}
public Claim GetClaim(string claimType)
{
return _context.HttpContext.User.FindFirst(x => x.Type == claimType);
}
}
我们有以下架构:
- .NET 核心 3.1 Web 应用程序使用剃刀页面、jQuery 等作为 UI(不是 angular 应用程序)
- .NET core 3.1 web api 应用程序作为我们的 api 层
- Okta 作为我们的身份提供者
我已经在 Web 应用程序中实现了 Okta 小部件和中间件。用户可以登录,之后我就可以获得 ClaimsPrincipal,访问他们的所有范围,并获取我通过 open id 存储的任何自定义配置文件数据。我可以通过 [Authorize] 装饰获得视图。一切正常。
我现在需要做的是在 API 端实施安全检查。我花了很多时间查看示例并发现了很多,但我要么遗漏了一些明显的东西,要么我所做的是独一无二的(我无法想象我所做的是那么独特)。基本上我需要做的是:
- 让网络应用程序将身份验证和 ID 令牌传递给 api
- 让 api 能够验证令牌,然后从 id 令牌中解密用户信息
这样我就可以在 API 端实施必要的安全逻辑。假设 return 的客户订单是 API - 我需要确保调用它的用户是管理员或实际客户(所以我不 return客户数据给不应该看到的人)。我已经弄清楚了所有角色的东西,我就是不能,为了我的生活,弄清楚如何通过令牌确定某人是谁?
传递令牌非常简单,但如何从 ClaimsPrincipal 对象中获取令牌?或者我是否需要在用户登录后调用 Okta API 来专门获取访问和 ID 令牌?
当然,我必须弄清楚如何让 API 端正确验证和解析发送的令牌。
如果任何人都可以帮助我开始这个或为我指出正确的方向作为例子,我将非常感激。至此,我已经阅读了我能找到的关于 Owin、OpenID、Okta、.net core 授权的所有文章。
您的 ID 提供者,在本例中为 Okta,将颁发一个 OpenID Connect 授权持有者令牌,您需要将该令牌传递给您想要保护的任何应用程序。
在应用程序的 Web Api 端,您需要注册中间件来处理 Okta 的 OpenID Connect 令牌。然后你可以用 [Authorize]
装饰你的 controllers/actions 并且你可以检查身份声明。
多亏了 Cameron Tinker 的建议,我才得以完成这项工作。有几件事让我很困惑,所以我会在这里分享它们,以防有人有同样的经历。
如果您使用的是 Okta,则可以通过 Okta 中间件包完成所有这些操作。您可以只使用 c# OpenID 库来完成,但 Okta.AspNetCore 库会有所帮助。
首先在网络应用中注册中间件。 Okta 在他们的网站上有很多这样的例子,而且非常简单。
在您的网络应用程序中,您可以使用它来获取令牌(当然是在用户通过身份验证之后)
await context.HttpContext?.GetTokenAsync("id_token")
通过标准机制将其作为 header 的一部分在您的 API 调用中发送:
"Authorization" : "Bearer [token]"
在 Web API 端,您使用相同的 Okta.AspNetCore 中间件包,然后可以使用 [Authorize] 装饰您的控制器以对它们强制执行身份验证。这是我被绊倒的地方。如果您没有使用 Okta 中的默认身份验证服务器并为您的应用程序设置了自定义服务器,则需要在配置中指定它和受众:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OktaDefaults.ApiAuthenticationScheme;
options.DefaultChallengeScheme = OktaDefaults.ApiAuthenticationScheme;
options.DefaultSignInScheme = OktaDefaults.ApiAuthenticationScheme;
})
.AddOktaWebApi(new OktaWebApiOptions()
{
OktaDomain = oktaDomain,
AuthorizationServerId = authServerId,
Audience = clientId
});
services.AddAuthorization();
我完全忘记了观众部分——根据令牌验证的工作方式,这部分是必需的。
从那里开始,中间件负责为您填充 ClaimsPrincipal,因此您可以通过 ClaimsPrincipal (HttpContext.User) 访问用户信息。我最终创建了一个 "CurrentUserService" 并将其拉出到它自己的库中,这样我就可以在那里整合我所有的身份验证处理程序;从而允许我的网络应用程序和网络 api 代码以相同的方式检查权限并检索有关当前用户的信息。如果您有兴趣,代码在这里:
public interface ICurrentUserService
{
public ClaimsPrincipal GetCurrentUser();
public string GetCurrentUserDisplayName();
public string GetCurrentUserFullName();
public string GetCurrentUserId();
public DateTime? GetCurrentUserDob();
public string GetCurrentUserGender();
public AddressFromClaimsDTO GetCurentUserAddress();
public bool IsAuthenticated();
}
public class CurrentUserService : ICurrentUserService
{
private const string FULL_ADDRESS_CLAIM_TYPE = "address";
private readonly IHttpContextAccessor _context;
public CurrentUserService(IHttpContextAccessor context)
{
_context = context;
}
/// <summary>
/// Gets whether or not the current user context is authenticated.
/// </summary>
/// <returns></returns>
public bool IsAuthenticated()
{
return GetCurrentUser().Identity.IsAuthenticated;
}
/// <summary>
/// Gets the current user's address.
/// TODO: tie this into our address data model... but if addresses live in Okta what does that mean?
/// </summary>
/// <returns></returns>
public AddressFromClaimsDTO GetCurentUserAddress()
{
var addressClaim = GetClaim(FULL_ADDRESS_CLAIM_TYPE);
if (addressClaim != null)
{
//var parseValue = addressClaim.Value.ToString().Replace("{address:", "{\"address\":");
var address = JsonSerializer.Deserialize<AddressFromClaimsDTO>(addressClaim.Value.ToString());
return address;
}
else
{
return new AddressFromClaimsDTO();
}
}
public ClaimsPrincipal GetCurrentUser()
{
return _context.HttpContext.User;
}
public string GetCurrentUserDisplayName()
{
return GetCurrentUser().Identity.Name;
}
public string GetCurrentUserFullName()
{
throw new NotImplementedException();
}
public string GetCurrentUserId()
{
throw new NotImplementedException();
}
public DateTime? GetCurrentUserDob()
{
var claim = GetClaim("birthdate");
if (claim != null && !string.IsNullOrEmpty(claim.Value))
{
return DateTime.Parse(claim.Value);
}
else
{
return null;
}
}
public string GetCurrentUserGender()
{
return GetClaim("gender")?.Value.ToString();
}
public Claim GetClaim(string claimType)
{
return _context.HttpContext.User.FindFirst(x => x.Type == claimType);
}
}