简体   繁体   中英

Dynamics Web API - Refresh Token

I am using the Dynamics Web API and Microsoft.IdentifyModel.Clients.ActiveDirectory package to connect to Dynamics from an Azure Function (.NET Core) app.

My connection is pretty standard,

        var clientCred = new ClientCredential(AppClientId, AppClientSecret);

            result = await _context.AcquireTokenAsync(DynamicsTenantUrl, clientCred);
            Token = result.AccessToken;

However, the application I have built this connection for has a long-running operation that eventually gets a "Not Authorized" back from Dynamics because the token has expired.

I have looked around at a couple of examples where I need to get the refresh_token back in order to then request that piece of information to keep the connection going, however the above does not give me a refresh token.

I have tried doing the AcquireTokenSilentAsync method and even though it shows a token in the TokenCache it always returns "No token available."

Is there a way to do this with the current implementation or do I need to change my connection code completely to access this token?

I wasn't able to figure how to do this with their Active Directory package. However it seems like that package is a fairly thin wrapper for oauth. Not sure why they're promoting a custom library to do something that's pretty wildly supported and general as the Oauth 2.0 protocoal.

By including offline_access in the scope for the oauth request I got a refresh token which can then be used to get as many access tokens as you need. There wasn't great documentation.

Getting the refresh token

NB: you'd want to use a general oauth 2 library with the web server flow to complete this

  1. GET https://login.microsoftonline.com/<tenant_id>/oauth2/authorize?client_id=<your_client_id>&response_type=code&redirect_uri=<your_callback_url>&response_mode=query&scope=offline_access&resource=<target_instance_url> (NB: you'll need other scopes depending on what you're doing with the token, ie for me it was offline_access https://admin.services.crm.dynamics.com/user_impersonation https://graph.microsoft.com/User.Read
  2. user authenticates and gets redirected to `?code=
  3. exchange authorization code for refresh token
  4. persist refresh token some where

Getting the access token

Now that you have a refresh token you can get access token as needed

POST https://login.microsoftonline.com/<tenant_id>/oauth2/token
client_id: <client_id>
scope: <scope>
grant_type: refresh_token
client_secret: <client_secret>
resource: <tenant_url>

NB: these would be form url encoded (typically handled by your oauth library for you)

Big picture you can use any standard oauth documentation for the web server and refresh token flows to guide you. The things that are ms dynamics specific are the scope values, the need to include the resource in your request (part of oauth, just not used that much in my experience)

Here is the C# webapi core sample, using ADAL lib that fetches if expired in Adal Cache. Replace D365 url, clientid, client secret and baseaddress.

namespace Microsoft.Crm.Sdk.Samples{
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Threading.Tasks;

public class Authentication
{
    private HttpMessageHandler _clientHandler = null;
    private AuthenticationContext _context = null;
    private string _authority = null;
    private string _resource = "https://xxxxx.api.crm.dynamics.com/";
    private string _clientId = "ddfsdfadsfdfadssddf";
    private string _clientSecret = "dsdfgfdgdfgfdgfd";


    public Authentication() { SetClientHandler(); }

    public AuthenticationContext Context
    {
        get
        { return _context; }

        set
        { _context = value; }
    }

    public HttpMessageHandler ClientHandler
    {
        get
        { return _clientHandler; }

        set
        { _clientHandler = value; }
    }


    public string Authority
    {
        get
        {
            if (_authority == null)
                _authority = DiscoverAuthority(_resource);

            return _authority;
        }

        set { _authority = value; }
    }

    public AuthenticationResult AcquireToken()
    {
        try
        {
            var clientA = new ClientCredential(_clientId, _clientSecret);
            return Context.AcquireTokenAsync(_resource, clientA).Result;

        }
        catch (Exception e)
        {
            throw new Exception("Authentication failed. Verify the configuration values are correct.", e); ;
        }

    }


    public static string DiscoverAuthority(string serviceUrl)
    {
        try
        {
            AuthenticationParameters ap = AuthenticationParameters.CreateFromUrlAsync(
                new Uri(serviceUrl + "api/data/")).Result;

            var strOAuth = ap.Authority.Substring(0, ap.Authority.LastIndexOf("/"));
            return strOAuth.Substring(0, strOAuth.LastIndexOf("/"));
        }
        catch (HttpRequestException e)
        {
            throw new Exception("An HTTP request exception occurred during authority discovery.", e);
        }
        catch (System.Exception e)
        {
            // This exception ocurrs when the service is not configured for OAuth.
            if (e.HResult == -2146233088)
            {
                return String.Empty;
            }
            else
            {
                throw e;
            }
        }
    }

    private void SetClientHandler()
    {

        _clientHandler = new OAuthMessageHandler(this, new HttpClientHandler());
        _context = new AuthenticationContext(Authority, true);

    }


    class OAuthMessageHandler : DelegatingHandler
    {
        Authentication _auth = null;

        public OAuthMessageHandler(Authentication auth, HttpMessageHandler innerHandler)
            : base(innerHandler)
        {
            _auth = auth;
        }

        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            // It is a best practice to refresh the access token before every message request is sent. Doing so
            // avoids having to check the expiration date/time of the token. This operation is quick.
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _auth.AcquireToken().AccessToken);

            return base.SendAsync(request, cancellationToken);
        }
    }
}

public class LongRunningOperation
{

    private HttpClient httpClient;

    private void ConnectToCRM(String[] cmdargs)
    {
        Authentication auth = new Authentication();
        httpClient = new HttpClient(auth.ClientHandler, true);
        httpClient.BaseAddress = new Uri("https://xxxxxx.api.crm.dynamics.com/" + "api/data/v9.1/");
        httpClient.Timeout = new TimeSpan(0, 2, 0);
        httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
        httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }

    private async System.Threading.Tasks.Task<Guid> CreateImportMap()
    {
        var importMap = new JObject();
        Console.WriteLine("--Section 1 started--");

        importMap.Add("name", "Import Map " + DateTime.Now.Ticks.ToString());
        importMap.Add("source", "Import Accounts.csv");
        importMap.Add("description", "Description of data being imported");
        importMap.Add("entitiesperfile", 1);

        HttpRequestMessage requestWebAPI =
            new HttpRequestMessage(HttpMethod.Post, "importmaps");
        requestWebAPI.Content = new StringContent(importMap.ToString(),
            Encoding.UTF8, "application/json");
        requestWebAPI.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
        HttpResponseMessage responseWebAPI =
            await httpClient.SendAsync(requestWebAPI);
        if (responseWebAPI.StatusCode == HttpStatusCode.NoContent)  //204  
        {
            Console.WriteLine("Import Map '{0} {1}' created.",
                importMap.GetValue("name"), importMap.GetValue("source"));
            string mapUri = responseWebAPI.Headers.
                GetValues("OData-EntityId").FirstOrDefault();
            string EntityId = mapUri.Substring(mapUri.IndexOf('(') + 1, 36);
            Console.WriteLine("Map URI: {0}", mapUri);
            return new Guid(EntityId);
        }
        else
        {
            Console.WriteLine("Failed to create ImportMap1 for reason: {0}",
                responseWebAPI.ReasonPhrase);
            throw new Exception(responseWebAPI.Content.ToString());
        }
    }
    public async void Run()
    {
        while (true)
        {
            Guid importMapId = await CreateImportMap();
            Thread.Sleep(10000);
        }
    }

    #region Main method

    static public void Main(string[] args)
    {
        try
        {
            var app = new LongRunningOperation();
            app.ConnectToCRM(args);
            app.Run();
        }

        catch (System.Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        finally
        {
            Console.WriteLine("Press <Enter> to exit.");
            Console.ReadLine();
        }
    }
    #endregion Main method

}}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM