jannikb
jannikb

Reputation: 524

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

I'm trying to implement an OAUth 2.0 flow for custom webapplication for Azure Devops. I'm following this https://learn.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)

Reproduction

I have registered an azdo app at https://app.vsaex.visualstudio.com/app/register and the authorize step seems to work fine, i.e. the user can authorize the app and the redirect to my app returns something that looks like a valid jwt token:

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
}

The next step is to get an access token which fails with a BadReqest: invalid_client, Failed to deserialize the JsonWebToken object.

Here is the full example:

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})")}");
        }
    }
}

Upvotes: 3

Views: 2100

Answers (1)

jannikb
jannikb

Reputation: 524

When asking for access tokens the Client Secret and not the App Secret must be provided for the client_assertion parameter:

enter image description here

Upvotes: 3

Related Questions