I have configured an Asp.net core 3.1 web api application that receives an azure AD token. I just can't find documentation that details what happens behind the scenes while the web api validates the token it receives. could someone explain how token validation happens at the background?
2 Answers
I assume you have a bit notice of the OIDC flows and claim types.
The Azure AD discovery document is located at a fixed place and is known if you have the tenant id (https://login.microsoftonline.com/{tenantId|'common'}/v2.0/.well-known/openid-configuration). This in combination with a client id is all that is required to complete the implicit flow of the authentication.
As soon as a request has the authorization header with the Bearer schema the middleware will start to decode the token. In the header of the token the type of hash and signature is noted together with the key id used. In the body you will have the claims present. The middleware will if the claims provided are valid. It at least check:
- if the authority claim matches with the issuer defined in the discovery document.
- the claim is still valid by verifying the not before and valid till claims with current time
- the issuer should match with the client id configured.
Then it will use the hash algorithm specified in the header of the token, to hash the header and the body. Using the key id from the header it will look up the public key published by the discovery document (the jwks_url will list all known public keys). To encrypt the hash and that result should match with the signature of the footer of the provided token which was created by the private part of that key.
If all this is valid then the middleware will add an identity to the claims principal with the list of this token and mark it as authenticated. It will not do any authorization in the application. That is handled by the next layer in your application.
In case that the token was invalid either expired, wrong authority or wrong signature. The middleware will not throw an error. It just not register the claims in the principal and the user remains anonymous.
This is a rough explanation and probably not 100% accurate of what goes on in the background of any JWT bearer authentication. Azure AD will follow the same flow, only configuration details are a bit different.
I think you need to register two applications in Azure, one representing the client application and the other representing the web api application (ie server application). Then use the user to log in to the client application to complete the authorization and obtain the access token, and then the client application uses the access token to call the api application.
The approximate process is as follows: When web api receives an access token, it first needs a filter to intercept the token, then parse the token and verify some key claims in the token, such as: aud
, sub
, scp
... After the claim in the token is verified, the back-end application will return the resource to the front-end.
Filter case:
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() {}
}
See a simple sample.