Asp.net 核心网站 api - 当网站 api 针对 Azure AD 验证 JWT 令牌时,幕后发生了什么
Asp.net core web api - what happens behind the scenes when the web api validates a JWT token against Azure AD
我已经配置了一个 Asp.net core 3.1 web api 应用程序来接收 azure AD 令牌。我只是找不到详细说明当网络 api 验证它收到的令牌时幕后发生的事情的文档。有人可以解释令牌验证是如何在后台进行的吗?
我认为您需要在 Azure 中注册两个应用程序,一个代表 client application and the other representing the web api application(即服务器应用程序)。然后使用用户登录客户端应用完成授权并获取access token,然后客户端应用使用access token调用api应用
大致流程如下: 当web api 收到访问令牌时,首先需要过滤器拦截令牌,然后解析令牌并验证令牌中的一些关键声明,例如: aud
, sub
, scp
... 验证token中的claim后,后台应用会return将资源发给前台
过滤条件:
package com.example.demo;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
//@Component
@WebFilter(filterName = "AdHelloFilter", urlPatterns = {"/ad/*"})
public class AdHelloFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse= (HttpServletResponse) response;
final String requestTokenHeader = httpRequest.getHeader("Authorization");
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
String jwtToken = requestTokenHeader.substring(7);
try {
DecodedJWT jwt = JWT.decode(jwtToken);
//judge if expired
Date expiresAt = jwt.getExpiresAt();
if(expiresAt.before(new Date())) {
Map<String, Object> errRes = new HashMap<String, Object>();
Map<String, Object> errMesg = new HashMap<String, Object>();
errMesg.put("code", "InvalidAuthenticationToken");
errMesg.put("message", "Access token has expired.");
errRes.put("error", errMesg);
String json = JSONObject.toJSONString(errRes);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, json);
return;
}
//judge if has specific scope
Claim a = jwt.getClaim("scp");
String scope = a.asString();
String[] scopeArr = scope.split(" ");
List<String> scopeList= Arrays.asList(scopeArr);
if(!(scopeList.contains("User.Read") && scopeList.contains("Mail.Read"))) {
Map<String, Object> errRes = new HashMap<String, Object>();
Map<String, Object> errMesg = new HashMap<String, Object>();
errMesg.put("code", "InvalidAuthenticationToken");
errMesg.put("message", "Unauthorized, pls add api permission");
errRes.put("error", errMesg);
String json = JSONObject.toJSONString(errRes);
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, json);
return;
}
} catch (JWTDecodeException exception){
System.out.println("Unable to Decode the JWT Token");
}
} else {
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
filterChain.doFilter(request, response);
}
@Override
public void init (FilterConfig filterConfig) throws ServletException{
System.out.println("init filter");
}
@Override
public void destroy() {}
}
看个简单的sample.
我假设您对 OIDC 流程和声明类型有所了解。
Azure AD 发现文档位于固定位置,并且如果您有租户 ID (https://login.microsoftonline.com/{tenantId|'common'}/v2.0/.well-known/openid-configuration) 就会知道。这与客户端 ID 相结合是完成身份验证的隐式流程所需的全部。
一旦请求获得了 Bearer 模式的授权 header,中间件就会开始解码令牌。在令牌的 header 中,哈希和签名的类型与使用的密钥 ID 一起被记录下来。在 body 中,您将拥有声明。如果提供的声明有效,中间件将会执行。它至少检查:
- 如果权威声明与发现文档中定义的发行者匹配。
- 通过验证不早于当前时间的索赔仍然有效
- 颁发者应与配置的客户端 ID 匹配。
然后它将使用令牌的 header 中指定的散列算法,对 header 和 body 进行散列。使用 header 中的密钥 ID,它将查找发现文档发布的 public 密钥(jwks_url 将列出所有已知的 public 密钥)。要加密散列,该结果应与由该密钥的私有部分创建的所提供令牌的页脚签名相匹配。
如果所有这些都是有效的,那么中间件将使用此令牌的列表向声明主体添加一个身份并将其标记为已验证。它不会在应用程序中做任何授权。这由应用程序中的下一层处理。
如果令牌无效,要么过期,要么权限错误,要么签名错误。中间件不会抛出错误。它只是不在主体中注册声明,用户保持匿名。
这是一个粗略的解释,可能不是 100% 准确地说明在任何 JWT 持有者身份验证的背景下发生的事情。 Azure AD 将遵循相同的流程,只是配置细节略有不同。
我已经配置了一个 Asp.net core 3.1 web api 应用程序来接收 azure AD 令牌。我只是找不到详细说明当网络 api 验证它收到的令牌时幕后发生的事情的文档。有人可以解释令牌验证是如何在后台进行的吗?
我认为您需要在 Azure 中注册两个应用程序,一个代表 client application and the other representing the web api application(即服务器应用程序)。然后使用用户登录客户端应用完成授权并获取access token,然后客户端应用使用access token调用api应用
大致流程如下: 当web api 收到访问令牌时,首先需要过滤器拦截令牌,然后解析令牌并验证令牌中的一些关键声明,例如: aud
, sub
, scp
... 验证token中的claim后,后台应用会return将资源发给前台
过滤条件:
package com.example.demo;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
//@Component
@WebFilter(filterName = "AdHelloFilter", urlPatterns = {"/ad/*"})
public class AdHelloFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse= (HttpServletResponse) response;
final String requestTokenHeader = httpRequest.getHeader("Authorization");
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
String jwtToken = requestTokenHeader.substring(7);
try {
DecodedJWT jwt = JWT.decode(jwtToken);
//judge if expired
Date expiresAt = jwt.getExpiresAt();
if(expiresAt.before(new Date())) {
Map<String, Object> errRes = new HashMap<String, Object>();
Map<String, Object> errMesg = new HashMap<String, Object>();
errMesg.put("code", "InvalidAuthenticationToken");
errMesg.put("message", "Access token has expired.");
errRes.put("error", errMesg);
String json = JSONObject.toJSONString(errRes);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, json);
return;
}
//judge if has specific scope
Claim a = jwt.getClaim("scp");
String scope = a.asString();
String[] scopeArr = scope.split(" ");
List<String> scopeList= Arrays.asList(scopeArr);
if(!(scopeList.contains("User.Read") && scopeList.contains("Mail.Read"))) {
Map<String, Object> errRes = new HashMap<String, Object>();
Map<String, Object> errMesg = new HashMap<String, Object>();
errMesg.put("code", "InvalidAuthenticationToken");
errMesg.put("message", "Unauthorized, pls add api permission");
errRes.put("error", errMesg);
String json = JSONObject.toJSONString(errRes);
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, json);
return;
}
} catch (JWTDecodeException exception){
System.out.println("Unable to Decode the JWT Token");
}
} else {
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
filterChain.doFilter(request, response);
}
@Override
public void init (FilterConfig filterConfig) throws ServletException{
System.out.println("init filter");
}
@Override
public void destroy() {}
}
看个简单的sample.
我假设您对 OIDC 流程和声明类型有所了解。
Azure AD 发现文档位于固定位置,并且如果您有租户 ID (https://login.microsoftonline.com/{tenantId|'common'}/v2.0/.well-known/openid-configuration) 就会知道。这与客户端 ID 相结合是完成身份验证的隐式流程所需的全部。
一旦请求获得了 Bearer 模式的授权 header,中间件就会开始解码令牌。在令牌的 header 中,哈希和签名的类型与使用的密钥 ID 一起被记录下来。在 body 中,您将拥有声明。如果提供的声明有效,中间件将会执行。它至少检查:
- 如果权威声明与发现文档中定义的发行者匹配。
- 通过验证不早于当前时间的索赔仍然有效
- 颁发者应与配置的客户端 ID 匹配。
然后它将使用令牌的 header 中指定的散列算法,对 header 和 body 进行散列。使用 header 中的密钥 ID,它将查找发现文档发布的 public 密钥(jwks_url 将列出所有已知的 public 密钥)。要加密散列,该结果应与由该密钥的私有部分创建的所提供令牌的页脚签名相匹配。
如果所有这些都是有效的,那么中间件将使用此令牌的列表向声明主体添加一个身份并将其标记为已验证。它不会在应用程序中做任何授权。这由应用程序中的下一层处理。
如果令牌无效,要么过期,要么权限错误,要么签名错误。中间件不会抛出错误。它只是不在主体中注册声明,用户保持匿名。
这是一个粗略的解释,可能不是 100% 准确地说明在任何 JWT 持有者身份验证的背景下发生的事情。 Azure AD 将遵循相同的流程,只是配置细节略有不同。