Azure Devops Oauth 身份验证:无法获取访问令牌(BadRequest 无法反序列化 JsonWebToken 对象)

Azure Devops Oauth authentication: Cannot get access token (BadRequest Failed to deserialize the JsonWebToken object)

我正在尝试为 Azure Devops 的自定义 Web 应用程序实施 OAUth 2.0 流程。我正在关注这个 https://docs.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops documentation as well as this https://github.com/microsoft/azure-devops-auth-samples/tree/master/OAuthWebSample OauthWebSample but using ASP.NET Core (I also read one issue on SO that looked similar but is not: Access Azure DevOps REST API with oAuth)

复制

我在 https://app.vsaex.visualstudio.com/app/register 注册了一个 azdo 应用, 授权步骤 似乎工作正常,即用户可以授权该应用并重定向到我的应用 returns 看起来像有效 jwt 令牌的东西:

header: {
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "oOvcz5M_7p-HjIKlFXz93u_V0Zo"
}
payload: {
  "aui": "b3426a71-1c05-497c-ab76-259161dbcb9e",
  "nameid": "7e8ce1ba-1e70-4c21-9b51-35f91deb6d14",
  "scp": "vso.identity vso.work_write vso.authorization_grant",
  "iss": "app.vstoken.visualstudio.com",
  "aud": "app.vstoken.visualstudio.com",
  "nbf": 1587294992,
  "exp": 1587295892
}

下一步是获取访问令牌,但失败并显示 BadReqest:invalid_client,无法反序列化 JsonWebToken 对象.

完整示例如下:

public class Config
{
    public string ClientId { get; set; } = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
    public string Secret { get; set; } = "....";
    public string Scope { get; set; } = "vso.identity vso.work_write";
    public string RedirectUri { get; set; } = "https://....ngrok.io/azdoaccount/callback";
}

/// <summary>
/// Create azdo application at https://app.vsaex.visualstudio.com/
/// Use configured values in above 'Config' (using ngrok to have a public url that proxies to localhost)
/// navigating to localhost:5001/azdoaccount/signin 
/// => redirect to https://app.vssps.visualstudio.com/oauth2/authorize and let user authorize (seems to work)
/// => redirect back to localhost:5001/azdoaccount/callback with auth code
/// => post to https://app.vssps.visualstudio.com/oauth2/token => BadReqest: invalid_client, Failed to deserialize the JsonWebToken object
/// </summary>
[Route("[controller]/[action]")]
public class AzdoAccountController : Controller
{
    private readonly Config config = new Config();
    [HttpGet]
    public ActionResult SignIn()
    {
        Guid state = Guid.NewGuid();

        UriBuilder uriBuilder = new UriBuilder("https://app.vssps.visualstudio.com/oauth2/authorize");
        NameValueCollection queryParams = HttpUtility.ParseQueryString(uriBuilder.Query ?? string.Empty);

        queryParams["client_id"] = config.ClientId;
        queryParams["response_type"] = "Assertion";
        queryParams["state"] = state.ToString();
        queryParams["scope"] = config.Scope;
        queryParams["redirect_uri"] = config.RedirectUri;

        uriBuilder.Query = queryParams.ToString();

        return Redirect(uriBuilder.ToString());
    }

    [HttpGet]
    public async Task<ActionResult> Callback(string code, Guid state)
    {
        string token = await GetAccessToken(code, state);
        return Ok();
    }

    public async Task<string> GetAccessToken(string code, Guid state)
    {
        Dictionary<string, string> form = new Dictionary<string, string>()
                {
                    { "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
                    { "client_assertion", config.Secret },
                    { "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" },
                    { "assertion", code },
                    { "redirect_uri", config.RedirectUri }
                };

        HttpClient httpClient = new HttpClient();

        HttpResponseMessage responseMessage = await httpClient.PostAsync(
            "https://app.vssps.visualstudio.com/oauth2/token",
            new FormUrlEncodedContent(form)
        );
        if (responseMessage.IsSuccessStatusCode) // is always false for me
        {
            string body = await responseMessage.Content.ReadAsStringAsync();
            // TODO parse body and return access token
            return "";
        }
        else
        {
            // Bad Request ({"Error":"invalid_client","ErrorDescription":"Failed to deserialize the JsonWebToken object."})
            string content = await responseMessage.Content.ReadAsStringAsync();
            throw new Exception($"{responseMessage.ReasonPhrase} {(string.IsNullOrEmpty(content) ? "" : $"({content})")}");
        }
    }
}

当请求访问令牌时,必须为 client_assertion参数: