如何编写 Azure API 管理策略以允许范围或角色

How do I author Azure API Management policy to allow either a scope or a role

我想限制 API 一组使用作用域的 AD 用户和一组使用应用角色的守护程序应用。但是,遵循 Azure APIM 策略将仅检查是否存在两个声明。我如何重写以下策略以允许范围或应用程序角色出现在 JWT 令牌中:

    <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized">
        <required-claims>
            <claim name="scp" match="any">
                <value>API.Request.Get</value>
            </claim>
            <claim name="roles" match="any">
                <value>API.Request.Get</value>                   
            </claim>
        </required-claims>
    </validate-jwt>

我的想法是结合choose whenjwt-validate,这是我的策略,它可以选择验证scp或角色,但我不知道为什么它不能正确验证值,我真的不是apim专家

<inbound>
        <base />
        <set-variable name="isScp" value="@{
            string isScp = "false";
            string authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
            if (authHeader?.Length > 0)
            {
                string[] authHeaderParts = authHeader.Split(' ');
                if (authHeaderParts?.Length == 2 && authHeaderParts[0].Equals("Bearer", StringComparison.InvariantCultureIgnoreCase))
                {
                    Jwt jwt;
                    if (authHeaderParts[1].TryParseJwt(out jwt))
                    {
                        if(jwt.Claims.GetValueOrDefault("scp", "null") != "null"){
                            isScp = "true";
                        }
                    }
                }
            }
            return isScp;
        }" />
        <choose>
            <when condition="@(context.Variables.GetValueOrDefault("isScp") == "false")">
                <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Server is unavailable for roles">
                    <required-claims>
                        <claim name="roles" match="any">
                            <value>User.ReadWrite.All</value>
                        </claim>
                    </required-claims>
                </validate-jwt>
            </when>
            <otherwise>
                <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Server is unavailable for scp">
                    <required-claims>
                        <claim name="scp" match="any" separator=" ">
                            <value>User.ReadWrite.All</value>
                        </claim>
                    </required-claims>
                </validate-jwt>
            </otherwise>
        </choose>
    </inbound>

对了,验证也可以写在代码里,不用validate-jwt,那就是:

<inbound>
        <base />
        <set-variable name="pass" value="@{
            bool isContainScp = false;
            bool isContainRoles = false;
            string pass = "false";
            string authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
            if (authHeader?.Length > 0)
            {
                string[] authHeaderParts = authHeader.Split(' ');
                if (authHeaderParts?.Length == 2 && authHeaderParts[0].Equals("Bearer", StringComparison.InvariantCultureIgnoreCase))
                {
                    Jwt jwt;
                    if (authHeaderParts[1].TryParseJwt(out jwt))
                    {
                        string tempScp = jwt.Claims.GetValueOrDefault("scp", "null");
                        if(tempScp != "null"){
                            isContainScp = tempScp.Contains("User.ReadWrite.All");
                        }else{
                            //write logic here
                            isContainRoles = true;
                        }
                    }
                }
            }
            if(isContainScp || isContainRoles){
                pass = "true";
            }
            return pass;
        }" />
        <choose>
            <when condition="@(context.Variables.GetValueOrDefault("pass") == "false")">
                <return-response response-variable-name="existing response variable">
                    <set-status code="401" reason="Unauthorized hhh" />
                </return-response>
            </when>
            <otherwise />
        </choose>
    </inbound>

感谢Tiny Wang 给我样片。 authHeader 已经包含令牌。因此,不需要进一步拆分它来获取令牌。这是更新的一个:

<inbound>
    <base />
    <set-variable name="clientType" value="@{
        bool isAuthorized = false;
        string authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
        
        if (authHeader?.Length == 0) {
            return "No Authorization Header";
        }
        Jwt jwt;
        if (!authHeader.TryParseJwt(out jwt)) {
            return "Parse JWT Token failed";
        }
        bool isPublicClient = jwt.Claims.GetValueOrDefault("azpacr", "null").Equals("0");
        bool isClientSecret = jwt.Claims.GetValueOrDefault("azpacr", "null").Equals("1");
        bool isClientCertificate = jwt.Claims.GetValueOrDefault("azpacr", "null").Equals("2");

        if (isPublicClient) {
            return "Public Client";
        } else if (isClientSecret) {
            return "Client Secret";
        } else if (isClientCertificate) {
            return "Client Certificate";
        }
        return "Unauthorized";
    }" />
    <choose>
        <when condition="@(context.Variables.GetValueOrDefault("clientType") == "Public Client")">
            <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized by APIM policy." require-expiration-time="false">
                <openid-config url="https://login.microsoftonline.com/YourTenantHere/v2.0/.well-known/openid-configuration" />
                <audiences>
                    <audience>Your audience here</audience>
                </audiences>
                <issuers>
                    <issuer>https://login.microsoftonline.com/YourTenantHere/v2.0</issuer> 
                </issuers>
                <required-claims>
                    <claim name="scp" match="any">
                        <value>API.Request.Get</value>
                    </claim>
                </required-claims>
            </validate-jwt>
            <return-response response-variable-name="existing response variable">
                <set-status code="200" reason="OK" />
            </return-response>
        </when>
        <when condition="@(context.Variables.GetValueOrDefault("clientType") == "Client Secret")">
            <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized by APIM policy." require-expiration-time="false">
                <openid-config url="https://login.microsoftonline.com/your tenant here/v2.0/.well-known/openid-configuration" /> 
                <audiences>
                    <audience>Your Audience Here</audience> 
                </audiences>
                <issuers>
                    <issuer>https://login.microsoftonline.com/Your tenant here/v2.0</issuer> 
                </issuers>
                <required-claims>
                    <claim name="roles" match="any">
                        <value>App.Request.Get</value>
                    </claim>
                </required-claims>
            </validate-jwt>
            <return-response response-variable-name="existing response variable">
                <set-status code="200" reason="OK" />
            </return-response>
        </when>
        <otherwise>
            <return-response response-variable-name="existing response variable">
                <set-status code="401" reason="Unauthorized hhh" />
            </return-response>
        </otherwise>
    </choose>
</inbound>

实现 JWT 角色声明的最佳途径是在 validate-jwt 策略中添加 'output-token-variable-name' 属性。 这是一个例子(这里要求的声明是 'emails' 但同样的想法):

<inbound>
    <base />
    <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid." require-scheme="Bearer" output-token-variable-name="jwt">
        <openid-config url="https://csmsorg.b2clogin.com/csmsorg.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_susi_reset_v2" />
        <audiences>
            <audience>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx</audience>
        </audiences>
        <issuers>
            <issuer>https://xxxxxx.b2clogin.com/e748014c-129c-4d13-accc-e5e1bb7c38eb/v2.0/</issuer>
        </issuers>
        <required-claims>
            <claim name="tfp" match="any">
                <value>B2C_1_susi_reset_v2</value>
                <value>B2C_1__SUSI</value>
            </claim>
        </required-claims>
    </validate-jwt>        
    <choose>
        <when condition="@(!((Jwt)context.Variables["jwt"]).Claims["emails"].Contains("xxxxxx@gmail.com"))">
            <return-response>
                <set-status code="403" reason="Forbidden" />
            </return-response>
        </when>
    </choose>
</inbound>