3

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})")}");
        }
    }
}
jannikb
  • 476
  • 1
  • 6
  • 16

1 Answers1

2

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

jannikb
  • 476
  • 1
  • 6
  • 16
  • Thanks for sharing your solution here, would you please accept your solution as the answer? So it would be helpful for other members who get the same issue to find the solution easily. Have a nice day:) – Hugh Lin Apr 22 '20 at 09:20
  • Thanks for the answers, Following your steps now I getting the following error . "Error": "unauthorized_client", "ErrorDescription": "Invalid length for a Base-64 char array or string." – Kumar S Sep 12 '20 at 12:23