Home
Options

Util.AzureCloud / Util.MSAL - can it create credentials for GraphServiceClient ?

I was blown away when I saw LinqPad's ability to simplify MSAL with MFA etc...

Truly great stuff, thanks ! :)

I've tried to create a Graph client using the token generated and I just can't get it right.

Anyone know how I can feed a Util.MSAL token into new GraphServiceClient ?

Thanks

Paul

Comments

  • Options

    Have you tried the approach suggested in the LINQPad Tutorial and Reference?

    Using a GraphServiceClient instead of an ArmClient:

    async Task Main()
    {
        string authEndPoint = Util.AzureCloud.PublicCloud.AuthenticationEndpoint;
        string tenantID = "domain.com";
        string userHint = $"user@{tenantID}";
    
        var credential = new LINQPadTokenCredential (authEndPoint + tenantID, userHint);
        var graphServiceClient = new GraphServiceClient (credential);
        ...    
    }
    
    class LINQPadTokenCredential : TokenCredential
    {
        public readonly string Authority, UserIDHint;
    
        public LINQPadTokenCredential (string authority, string userIDHint) => (Authority, UserIDHint) = (authority, userIDHint);
    
        public override AccessToken GetToken (TokenRequestContext requestContext, CancellationToken cancellationToken)
            => GetTokenAsync (requestContext, cancellationToken).Result;
    
        public override async ValueTask<AccessToken> GetTokenAsync (TokenRequestContext requestContext,
                                                                    CancellationToken cancelToken)
        {
            // Call LINQPad's AcquireTokenAsync method to authenticate interactively, and cache token in the LINQPad GUI.
            var auth = await Util.MSAL.AcquireTokenAsync (Authority, requestContext.Scopes, UserIDHint).ConfigureAwait (false);
            return new AccessToken (auth.AccessToken, auth.ExpiresOn);
        }
    }
    
  • Options

    Thanks Joe,

    I'm missing something, I'm getting "Access is denied. Check credentials and try again." on the inner exception when using the graph client.
    I think I need to set an App (with a client id) in Azure.
    The outer exception is "Exception of type 'Microsoft.Graph.Models.ODataErrors.ODataError' was thrown."

    After creating the graphServiceClient, I run.... this is where the access denied comes in

    var result = await graphServiceClient.Me.Messages.GetAsync((requestConfiguration) =>
    {
        requestConfiguration.QueryParameters.Filter = "importance eq 'high'";
    }); 
    

    I'm using MFA, could that be the problem? The other example using Util.AzureCloud.PublicCloud works perfectly, but I can't seem to use that token for Microsoft Graph...

    string azureAuth = Util.AzureCloud.PublicCloud.AuthenticationEndpoint;
    string scope = Util.AzureCloud.PublicCloud.ManagementApiDefaultScope;
    
    // Now we can ask the LINQPad host process to authenticate:
    var authResult = await Util.MSAL.AcquireTokenAsync (azureAuth + tenantID, scope, userHint);
    
  • Options

    What happens when you try the code I posted?

  • Options
    edited July 2023

    Hi Joe,

    It throws the exception I mentioned.
    The outer exception is "Exception of type 'Microsoft.Graph.Models.ODataErrors.ODataError' was thrown."
    Main error gives

    • Message: Access is denied. Check credentials and try again.

    I'm using your LINQPadTokenCredential class

    My main method is almost identical to your example, adding the GraphServiceClient...

    async Task Main()
    {
    string authEndPoint = Util.AzureCloud.PublicCloud.AuthenticationEndpoint;
    
    string domain = Util.ReadLine("Enter the domain");
    string username = Util.ReadLine("Enter the username");
    
    string tenantID = domain;
    string userHint = $"{username}@{tenantID}";
    
    var credential = new LINQPadTokenCredential(authEndPoint + tenantID, userHint);
    var graphServiceClient = new GraphServiceClient(credential);
    
    try
    {
        var result = await graphServiceClient.Me.Messages.GetAsync((requestConfiguration) =>
        {
        requestConfiguration.QueryParameters.Filter = "importance eq 'high'";
        });
    }
    catch (Microsoft.Graph.Models.ODataErrors.ODataError ex)
    {
        ex.Dump();
    }
    }
    

    My credentials are good. I know this because if I use your example "Authentication with MSAL (interactive + MFA + Azure + OAuth).linq", and paste the token received into https://jwt.ms, I get a good response.

  • Options

    Are you confident that the config on the Azure end is set up correctly to allow these credentials?

  • Options

    I should have asked this question a different way, I still have a lot to learn about Azure AD, Auth and Graph

    I can create a GraphServiceClient using a DeviceCodeCredential (Azure.Identity) and setting user scopes. To use this method I also need to register an App in Azure AD. This gives me a clientId and tenantId that I can use when instantiating the GraphServiceClient.

    Using the LinqPad MSAL method, do I need to create the App in Azure Id, and how do I integrate the tenantId, clientId and scopes into the LinqPad MSAL method ?

    My preference is to be able to access the Graph ".Me" methods (ie: userClient.Me.MailFolders["Inbox"].Messages) without needing to register an App in Azure AD, which is what I figured the LinqPad MSAL method gave me.

  • Options
    edited February 11

    I found the time to come back to this on the weekend.

    I wanted to authenticate with AzureAD and send an email using Microsoft Graph. I figured LinqPads new LINQPadTokenCredential would make this a breeze for me.

    In the end, I resolved that what I was trying to do was impossible using the LINQPadTokenCredential method, so I used the traditional method.

    Reasons Why:

    Limited Scopes available from Util.AzureCloud.PublicCloud
    I'm targeting a "Mail.Send" scope. It wasn't clear to me how I could create my own scope in Util.MSAL.AcquireTokenAsync. Using "Mail.Send", "https://graph.microsoft.com/.default" or a blank scope worked.

    Needed to use a ClientId (AD App Regisgration)
    I figured that I should be able to use LINQPadTokenCredential without having to create an App Registration in Azure AD. In the end, the easiest method was to create an App Registration, but there's no need for a secret using the method I used. It's sending as the logged user, not on behalf of the user.

    **Set API Permissions, in the App Registration **
    Mail.Send (from Microsoft Graph) must be set as in API Permission.

    Needed a Redirect URL
    Specify a redirect url, for LinqPad (or for a Desktop App), that's https://login.microsoftonline.com/common/oauth2/nativeclient

    using Azure.Identity;
    using Microsoft.Graph;
    async void Main()
    {
    
        var tenantId = "xxx" ; // From the App Registration 
        var clientId = "xxx"; // From the App Registration 
    
        var options = new InteractiveBrowserCredentialOptions
        {
            AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
            ClientId = clientId,
            TenantId = tenantId,
            RedirectUri = new Uri("https://login.microsoftonline.com/common/oauth2/nativeclient")
        };
    
    
    // for mail.send any of the following scopes worked, I found that if you’re doing Mail.ReadWrite, then I need to use the second method, specifying the additional scopes
        var scopes = new[] { "" };
    //  var scopes = new[] { "Mail.Send" };
    //  var scopes = new[] { "https://graph.microsoft.com/.default" };
    
    
        var credential = new InteractiveBrowserCredential(options);
        var graphClient = new GraphServiceClient(credential, scopes);
    
        var subject = $"Test email, send {DateTime.Now.ToString("s")}";
        var body = $"Email Message . Device: {Environment.MachineName}. User: {Environment.UserName}.";
    
    
        var message = new Message
        {
            Subject = subject,
            Body = new ItemBody
            {
                ContentType = BodyType.Html,
                Content = body
            },
            ToRecipients = new List<Recipient>()
        {
            new Recipient { EmailAddress = new EmailAddress { Address = "test@example.com" }}
        }
        };
    
    
        Microsoft.Graph.Me.SendMail
            .SendMailPostRequestBody requestbody = new()
        {
            Message = message,
            SaveToSentItems = true // or false
        };
    
        await graphClient.Me.SendMail.PostAsync(requestbody);
    
    }
    
  • Options

    Thanks for the info.

    You can specify scopes and a client ID in the Util.MSAL.AcquireTokenAsync method. Have you tried the following:

    class LINQPadTokenCredential : TokenCredential
    {
        public readonly string Authority, UserIDHint;
    
        public LINQPadTokenCredential (string authority, string userIDHint) => (Authority, UserIDHint) = (authority, userIDHint);
    
        public override AccessToken GetToken (TokenRequestContext requestContext, CancellationToken cancellationToken)
            => GetTokenAsync (requestContext, cancellationToken).Result;
    
        public override async ValueTask<AccessToken> GetTokenAsync (TokenRequestContext requestContext,
                                                                    CancellationToken cancelToken)
        {
            // Call LINQPad's AcquireTokenAsync method to authenticate interactively, and cache token in the LINQPad GUI.
            var auth = await Util.MSAL.AcquireTokenAsync (
                Authority,
                ["user.read", "mail.read", "mail.send"],
                UserIDHint,
                "<Your client ID>").ConfigureAwait (false);
    
            return new AccessToken (auth.AccessToken, auth.ExpiresOn);
        }
    }
    
  • Options

    Thanks Joe,
    Yes, that worked, and taught me a lot about MSAL in the process.
    Paul

  • Options
    edited May 6

    Suggestion: include the LINQPadTokenCredential class within LINQPad itself.

    I was able to get GraphServiceClient working with the provided LINQPadTokenCredential class and a couple small modifications. Part of the trick was to update my app registration to have a callback of https://login.microsoftonline.com/common/oauth2/nativeclient

    This GitHub issue was helpful: https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/2127

    Manually modified the app manifest

    "replyUrlsWithType": [
       {
          "url": "https://login.microsoftonline.com/common/oauth2/nativeclient",
          "type": "InstalledClient"
       }
    ]
    

    Set the Allow public client flows toggle to Yes.

    Code

    async Task Main()
    {
        string tenantID = "yourdomain.com,net";
        string userHint = $"user.name@{tenantID}";
        string clientID = "your_client_id_value";
    
        // this is needed in the app registration
        //  "replyUrlsWithType": [{"url": "https://login.microsoftonline.com/common/oauth2/nativeclient","type": "InstalledClient"}],
    
        var credential = new LINQPadTokenCredential(Util.AzureCloud.PublicCloud.AuthenticationEndpoint + tenantID, userHint, clientID, new List<string> { "user.read", "mail.send" }.ToArray());
        var gsc = new GraphServiceClient(credential);
        try
        {
            var subject = $"Test email, send {DateTime.Now.ToString("s")}";
            var body = $"Email Message. User: {Environment.UserName}.";
    
    
            var message = new Message
            {
                Subject = subject,
                Body = new ItemBody
                {
                    ContentType = BodyType.Html,
                    Content = body
                },
                ToRecipients = new List<Recipient>()
                {
                    new Recipient { EmailAddress = new EmailAddress { Address = "recipient_email_here" }}
                }
            };
    
            Microsoft.Graph.Me.SendMail
                .SendMailPostRequestBody requestbody = new()
                {
                    Message = message,
                    SaveToSentItems = true // or false
                };
    
            await gsc.Me.SendMail.PostAsync(requestbody);
    
    
            var result = await gsc.Me.GetAsync();
            result.Dump("result");
        }
        catch (Microsoft.Graph.Models.ODataErrors.ODataError ex)
        {
            ex.Dump();
        }
    }
    
    class LINQPadTokenCredential : TokenCredential
    {
        public readonly string Authority, UserIDHint, ClientId;
    
        public readonly string[] Scopes;
    
        public LINQPadTokenCredential(string authority, string userIDHint, string clientId, string[] scopes) => (Authority, UserIDHint, ClientId, Scopes) = (authority, userIDHint, clientId, scopes);
    
        public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) => GetTokenAsync(requestContext, cancellationToken).Result;
    
        public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancelToken)
        {
            // Call LINQPad's AcquireTokenAsync method to authenticate interactively, and cache token in the LINQPad GUI.
            var auth = await Util.MSAL.AcquireTokenAsync(Authority, Scopes, UserIDHint, ClientId, LINQPad.Util.MSAL.Prompt.NoPromptUnlessNecessary).ConfigureAwait(false);
            return new AccessToken(auth.AccessToken, auth.ExpiresOn);
        }
    }
    
  • Options

    Thanks for the info.

    The difficulty with including LINQPadTokenCredential in LINQPad is that it would need to be dynamically built in order to avoid creating a dependency on a specific version of Azure.Core. While this could be done with some automagic, it would get very messy if MS were to change the base class in the future, as LINQPad would need to figure out which version of Azure.Core you're referencing, and write a different version of LINQPadTokenCredential in each case.

    It would also make it harder to diagnose and debug - right now, you can edit that class and set a breakpoint in GetTokenAsync to figure out exactly what's going on.

Sign In or Register to comment.