Tuesday, May 11, 2021

New task group is not visible in edit Azure DevOps pipeline window

If you have created new task group in Azure DevOps and want to add it to your pipeline you may face with the problem that this task group won't be visible in Add task window (I'm talking now about using visual designer when edit pipeline, not by edit yaml file). Even if you stop edit and edit pipeline again - it won't appear. In this case try to click Refresh link on the top - it will fetch latest results from DevOps and your task should appear after that. Note that you should search by task group name:



Wednesday, May 5, 2021

How to suppress warning "TenantId ... contains more than one active subscription. First one will be selected for further use" when use Connect-AzAccount

When you use Connect-AzAccount cmdlet from Az.Accounts module and there are several subscriptions in your tenant it will show the following warning:


"TenantId ... contains more than one active subscription. First one will be selected for further use. To select another subscription, use Set-AzContext"

If you want to handle this situation and give user possibility to explicitly select needed subscription then this warning may not be needed (as it may confuse end users). In order to suppress it use "-WarningAction Ignore" parameter:

Connect-AzAccount -WarningAction Ignore

In this case warning won't be shown:

You may use this technique for suppressing warning from other cmdlets too.

Friday, April 30, 2021

Provision Azure Storage table via ARM template

It is possible to provision Azure Storage table via New-AzStorageTable cmdlet. However it is also possible to provision it via ARM template and New-AzResourceGroupDeployment cmdlet. Last technique is quite powerful because allows to provision many different Azure resources in universal way. In order to provision Azure Storage table via ARM template use the following template:

"resources": [
{
  "type": "Microsoft.Storage/storageAccounts",
  "name": "[parameters('storageAccountName')]",
  "apiVersion": "2019-04-01",
  "kind": "StorageV2",
  "location": "[parameters('location')]",
  "sku": {
	"name": "Standard_LRS"
  },
  "properties": {
	"supportsHttpsTrafficOnly": true
  }
},
{
	"name": "[concat(parameters('storageAccountName'),'/default/','Test')]",
	"type": "Microsoft.Storage/storageAccounts/tableServices/tables",
	"apiVersion": "2019-06-01",
	"dependsOn": [
		"[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]"
	]
},

In this example we provision both Azure storage and then table Test in this Azure storage. It is also possible to provision only table - in this case use only second part of template.

Monday, April 26, 2021

Calculate Azure AD groups count via MS Graph in PowerShell

If you need to fetch Azure AD groups or e.g. calculate total count of AAD groups via MS Graph API in PowerShell you may use Powershell-MicrosoftGraph project on github. At first you need to clone repository locally and copy it's folder to local PowerShell Modules folder:

git clone 'https://github.com/Freakling/Powershell-MicrosoftGraph'
Copy-item -Path "Powershell-MicrosoftGraph\MicrosoftGraph\" -Destination ($env:PSModulePath.Split(';')[-1]) -recurse -force

We will make Graph requests using app permissions. It means that you need to have registered AAD app with permissions Groups.Read.All for fetching the groups:


Copy clientId and clientSecret of this AAD app and tenantId of your tenant (you may copy it from Azure portal > Azure AD overview tab). Having all this data in place run the following script:

$appID = "..."
$appSecret = "..."
$tenantID = "..."
$credential = New-Object System.Management.Automation.PSCredential($appID,(ConvertTo-SecureString $appSecret -AsPlainText -Force))
$token = Get-MSGraphAuthToken -credential $credential -tenantID $tenantID
(Invoke-MSGraphQuery -URI 'https://graph.microsoft.com/v1.0/groups' -token $token -recursive -tokenrefresh -credential $credential -tenantID $tenantID | select -ExpandProperty Value | measure).Count

It will output total count of groups in your AAD.

Thursday, April 22, 2021

Change "Allow public client flows" property of Azure AD apps via PowerShell

Some time ago I wrote about several problems related with changing of "Allow public client flows" property of Azure AD apps (on the moment when this post has been written this property was called in UI differently "Default client type > Treat application as public client". Nowadays it is called "Allow public client flows"): Several problems when use Set-AzureADApplication cmdlet with AzureAD app with allowPublicClient = true.

The problem was that it was not possible to change this setting from PowerShell script via Set-AzureADApplication cmdlet. However it is still possible to change it from script (solution was found by Piotr Satka so all credits go to him) - you need to use another cmdlets Get-AzureADMSApplication and Set-AzureADMSApplication. Here is the sample:

$azureAdMsApps = Get-AzureADMSApplication 
$azureAdMsApp = $azureAdMsApps | Where-Object { $_.AppId -eq $appId }
Set-AzureADMSApplication -ObjectId $azureAdMsApp.Id -IsFallbackPublicClient $value | Out-Null

Using this code you will be able to change "Allow public client flows" property for Azure AD apps via PowerShell.

Friday, April 9, 2021

How to test certificate-based authentication in Azure functions for Sharepoint Online on local PC

If you develop Azure function you probably often run them locally on dev PC rather than in Azure. It simplifies debugging and development. In this post I will show how to test certificate-based authentication for Sharepoint Online in Azure functions running locally. First of all we need to register AAD app in Azure portal and grant it Sharepoint permissions:

Don't forget to grant Admin consent after adding permissions.

After that generate self-signed certificate using Create-SelfSignedCertificate.ps1 script from here: Granting access via Azure AD App-Only:

.\Create-SelfSignedCertificate.ps1 -CommonName "MyCertificate" -StartDate 2021-04-08 -EndDate 2031-04-09

It will generate 2 files:

  • private key: .pfx
  • public key: .cer

Go to registered AAD app > Certificates & secrets > Certificates > Upload certificate and upload generated .cer file. After upload copy certificate thumbprint - it will be needed for Azure functions below.

In Azure function certificate-based authentication for Sharepoint Online can be done by the following code (using OfficeDevPnP.Core):

using (var authMngr = new OfficeDevPnP.Core.AuthenticationManager())
{
    using (var ctx = authMngr.GetAzureADAppOnlyAuthenticatedContext(siteUrl, clientId, tenant, StoreName.My, StoreLocation.CurrentUser, certificateThumbprint))
    {
        ...
    }
}

Here we specified clientId of our AAD app, copied certificate thumbprint and tenant in the form {tenant}.onmicrosoft.com.

Before to run it we need to perform one extra step: install certificate to local PC certificates store. It can be done by double click on .pfx file. After that Windows will open Certificate import wizard:


Since our code is using Personal store use Store Location = Current User. Then specify password and import your certificate to the store. You may check that certificate is installed properly by opening MMC console > Add/Remove snapin > Certificates. Imported certificate should appear under Personal > Certificates:

After that you will be able to run Azure functions locally which communicate with Sharepoint Online using certificate-based authentication.

Tuesday, March 30, 2021

Create new site collections in Sharepoint Online using app-only permissions with certificate-based authentication (without appinv.aspx and client secret)

In the past if we needed to create site collections in Sharepoint Online we registered new SP app on appregnew.aspx page and granted Full control permissions on tenant level using appinv.aspx (alternatively instead of appregnew.aspx we could register new AAD app in Azure portal and then use it's clientId on appinv.aspx). But nowadays it is not preferred way to achieve this goal. In this article I will show ho to create new site collections using app-only permissions with certificate-based authentication (without appinv.aspx and client secret).

First of all we need to register new AAD app in Azure portal and grant Sites.FullControl.All API permissions (after granting permissions admin consent will be also needed):


Now for testing purposes let's try to create Modern Communication site using clientId and clientSecret:

using (var ctx = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext("https://{tenant}-admin.sharepoint.com", "{clientId}", "{clientSecret}"))
{
    ctx.RequestTimeout = Timeout.Infinite;
    var tenant = new Tenant(ctx);
    var properties = new SiteCreationProperties
    {
        Url = "https://{tenant}.sharepoint.com/sites/{siteUrl}",
        Lcid = 1033,
        TimeZoneId = 59,
        Owner = "{username}@{tenant}.onmicrosoft.com",
        Title = "{siteTitle}",
        Template = "SITEPAGEPUBLISHING#0",
        StorageMaximumLevel = 0,
        StorageWarningLevel = 0
    };

    var op = tenant.CreateSite(properties);
    ctx.Load(tenant);
    ctx.Load(op);
    ctx.ExecuteQueryRetry();

    ctx.Load(op, i => i.IsComplete);
    ctx.ExecuteQueryRetry();

    while (!op.IsComplete)
    {
        Thread.Sleep(30000);
        op.RefreshLoad();
        ctx.ExecuteQueryRetry();
        Console.Write(".");
    }
    Console.WriteLine("Site is created");
}

If we will try to run this code we will get Microsoft.SharePoint.Client.ServerUnauthorizedAccessException:

Access denied. You do not have permission to perform this action or access this resource.

Now let's try to create self-signed certificate like described here: Granting access via Azure AD App-Only. Then upload public key (.cer file) to our AAD app:


After that let's change C# example shown above to use certificate private key (.pfx file) and password for authentication. Code for creating site collection will remain the same:

using (var ctx = new OfficeDevPnP.Core.AuthenticationManager().GetAzureADAppOnlyAuthenticatedContext(
    "https://{tenant}-admin.sharepoint.com",
    "{clientId}",
    "{tenant}.onmicrosoft.com",
    @"C:\{certFileName}.pfx",
    "{certPassword}"))
{
    ctx.RequestTimeout = Timeout.Infinite;
    var tenant = new Tenant(ctx);
    var properties = new SiteCreationProperties
    {
        Url = "https://{tenant}.sharepoint.com/sites/{siteUrl}",
        Lcid = 1033,
        TimeZoneId = 59,
        Owner = "{username}@{tenant}.onmicrosoft.com",
        Title = "{siteTitle}",
        Template = "SITEPAGEPUBLISHING#0",
        StorageMaximumLevel = 0,
        StorageWarningLevel = 0
    };

    var op = tenant.CreateSite(properties);
    ctx.Load(tenant);
    ctx.Load(op);
    ctx.ExecuteQueryRetry();

    ctx.Load(op, i => i.IsComplete);
    ctx.ExecuteQueryRetry();

    while (!op.IsComplete)
    {
        Thread.Sleep(30000);
        op.RefreshLoad();
        ctx.ExecuteQueryRetry();
        Console.Write(".");
    }
    Console.WriteLine("Site is created");
}

This code will work and site collection will be successfully created. This is how you may create site collections in Sharepoint Online using app-only permissions and certificate-based authentication.

Tuesday, March 16, 2021

Attach onClick handler on div dynamically in runtime using css class selector in React and Fluent UI

Sometimes in React we may need to attach javascript handler on html element using "old" way which closer to DOM manipulation which we did in pure javascript code. I.e. if you can't get component reference because of some reason and the only thing you have is css class of html element in the DOM you may still need to work with it via DOM manipulations.

As example we will use Search element from Fluent UI. It renders magnifier icon internally and it is quite hard to get component ref on it. Highlighted magnifier is rendered as div with "ms-SearchBox-iconContainer" css class:

Imagine that we want to attach onClick handler on magnifier icon so users will be able to click it and get search results. Here is how it can be achivied:

private _onClick(self: Search) {
  // search logic
}

public componentDidMount() {
  let node = document.querySelector(".ms-SearchBox-iconContainer");
  if (node) {
    node.addEventListener("click", e => this._onClick(this));
  }
}

public componentWillUnmount() {
  let node = document.querySelector(".ms-SearchBox-iconContainer");
  if (node) {
    node.removeEventListener("onClick", e => this._onClick(this));
  }
}

So we add handler in componentDidMount and remove it componentWillUnmount. Inside these methods we use document.querySelector() for finding actual div to which we need to attach onClick handler. Note that we pass this as parameter of _onClick since this inside this method will point to div html element but not to search component itself.

Wednesday, March 10, 2021

Get Sharepoint site collection id (SPSite.ID) and web id (SPWeb.ID) from followed sites returned from REST API

In Sharepoint we can fetch all followed sites for the current user by the following REST API endpoint:

http://example.com/_api/social.following/my/Followed(types=4)

(instead of http://example.com you should use url of your SP site). It will return collection of site objects which will contain such properties as name, url, etc. However often we need to know also site collection id (SPSite.ID) and web id (SPWeb.ID). We can of course go through all returned sites and fetch their ids by separate JSOM/CSOM calls but it will affect performance (it is classic n+1 problem when we at first get list of items (1st call) and then for each item in the list make separate API call (n calls)).

Fortunately it is possible to get these ids right from REST API response. There is one strange field called "id" which looks like this:

"Id": "8.b4af2aa5fb834daa87aa9fb4155abd7d.b14bd3b3d6084dadb0fc7b79679fc767.
b4af2aa5fb834daa87aa9fb4155abd7d.00000000000000000000000000000000",

So there are several strings divided by dot. If we will check site and web id we will see that second string looks like SPSite.ID and 3rd string like SPWeb.ID:


The only difference is that they don't contain dashes. But it is quite easy do add them by ourselves in the code:

id.substr(0, 8) + "-" + id.substr(8, 4) + "-" + id.substr(12, 4) + "-" + id.substr(16, 4) + "-" + id.substr(20)

Using this approach we can get ids of Sharepoint site collections and web sites directly from REST API response.

Thursday, February 25, 2021

Fetch Sharepoint Online sites which are not associated with O365 groups via Sharepoint Search KQL

In the previous article I showed how we can use Search API in order to fetch sites which are associated with O365 groups:

(contentclass:STS_Site OR contentclass:STS_Web) GroupId<>""

But what if we need to get opposite result: fetch only those sites which are not associated with O365 groups (sites without groups). Our knowledge about GroupId indexed property bag property will help also here since we need to fetch those sites which don't have any value in GroupId property.

First (naive) attempt to do that would be trying something like that:

(contentclass:STS_Site OR contentclass:STS_Web) GroupId:""

However it won't work because KQL supports not equal operation with empty string but doesn't support equal to empty string operation. My colleague Juha Alhojoki pointed me to the original trick which allows to do that: Check Not Null condition in Keyword Query Language for SharePoint Search. So idea is that if we know possible start letters for managed property (remember that KQL supports prefix matching but doesn't support suffix matching, i.e. we may search by "StartsWith" operator but not with "EndsWith") we may enumerate them all and then apply NOT operator to the final condition.

In our example GroupId contains guid i.e. it may start with Latin alphabet symbols and digits. So we will have the following condition to fetching sites which are not associated with O365 groups:

(contentclass:STS_Site OR contentclass:STS_Web) NOT(GroupId:a* OR GroupId:b* OR GroupId:c* OR GroupId:d* OR GroupId:e* OR GroupId:f* OR GroupId:g* OR GroupId:h* OR GroupId:i* OR GroupId:j* OR GroupId:k* OR GroupId:l* OR GroupId:m* OR GroupId:n* OR GroupId:o* OR GroupId:p* OR GroupId:q* OR GroupId:r* OR GroupId:s* OR GroupId:t* OR GroupId:u* OR GroupId:v* OR GroupId:w* OR GroupId:x* OR GroupId:y* OR GroupId:z* OR GroupId:0* OR GroupId:1* OR GroupId:2* OR GroupId:3* OR GroupId:4* OR GroupId:5* OR GroupId:6* OR GroupId:7* OR GroupId:8* OR GroupId:9*)

What we did here is enumerated all Latin symbols and digits and applied NOT operator to exclude sites which match enumerated criterias.

Thursday, February 18, 2021

Fetch Sharepoint Online sites associated with O365 groups via Sharepoint Search KQL

As you probably know O365 groups have associated Sharepoint Online site behind. It may be needed to fetch only those sites which have associated O365 group via Sharepoint Search and KQL. In this post I will show how to do that and in the future post I will describe how to fetch only those sites which are not associated with O365 groups.

Modern Team sites which are associated with O365 group are created using Groups template. I.e. in order to get all sites associated with groups we may use the following KQL query:

(contentclass:STS_Site OR contentclass:STS_Web) WebTemplate:GROUP

It will work however there may be problem with old sites which were "groupified". I.e. regular site could be connected to O365 site - this procedure is called groupifying (see Connect to a Microsoft 365 group). Above query won't return such sites.

Another approach is based on the fact that groupified sites contain special property bag properties which contain information about connected group:

  • GroupAlias
  • GroupDocumentsListId
  • GroupDocumentsUrl
  • GroupId
  • GroupType

By default custom property bag properties are not searchable but they can be made searchable by adding them to Indexed Property Bag which can be done via PowerShell:

Connect-PnPOnline -Url "http://{tenant}.sharepoint.com"
Set-PnPPropertyBagValue -Key "customProperty" -Value "foo" -Indexed  

Here is good article which explains this topic: What Are Indexed Property Bags And How To Use Them For SharePoint Site.

The good thing is that at least GroupId looks like indexed by default. This gives us possibility to use another KQL for getting sites connected with O365 groups:

(contentclass:STS_Site OR contentclass:STS_Web) GroupId<>""

This query will work also with groupified sites.

Update 2021-02-25: see also article which shows how to fetch sites which are not associated with O365 groups: Fetch Sharepoint Online sites which are not associated with O365 groups via Sharepoint Search KQL.

Wednesday, February 17, 2021

Get list items from Sharepoint using lists.asmx web service

In one of my previous articles I showed how to get authentication cookies (FedAuth) from Sharepoint FBA using authentication.asmx web service (see Authenticate in Sharepoint on-prem FBA site via OTB /_vti_bin/Authentication.asmx web service). In this post I will show how to use these cookies and get list items from Sharepoint list using standard lists.asmx web service.

It can be done using the following code:

Cookie authCookies = ...;
string listTitle = ...;
string soapBody =
	"<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>" +
	"  <soap:Body>" +
	"    <GetListItems xmlns='http://schemas.microsoft.com/sharepoint/soap/'>" +
	"      <listName>" + listTitle + "</listName>" +
	"    </GetListItems>" +
	"  </soap:Body>" +
	"</soap:Envelope>";
var cookies = new CookieContainer();
cookies.Add(authCookies);
var handler = new HttpClientHandler();
handler.CookieContainer = cookies;
using (var httpClient = new HttpClient(handler))
{
	var req = new HttpRequestMessage(HttpMethod.Post, "http://example.com/_vti_bin/lists.asmx")
	{
		Content = new StringContent(soapBody, Encoding.UTF8, "text/xml")
	};
	var res = httpClient.SendAsync(req).GetAwaiter().GetResult();
	string result
	if (res.StatusCode == HttpStatusCode.OK)
	{
		result = res.Content.ReadAsStringAsync().Result;
	}
}

Here we use auth cookies from previous article. We again create special SOAP body where pass list title. In this example we will get all list items but it is also possible to fetch list items which match CAML query - for that you will need to use quert, viewFieldsm rowLimit and queryOptions tags in SOAP body. For more information about these properties refer to documentation of lists.asmx web service. Id everything was done correctly it will return you list items from specified lists in SOAP format.

Friday, February 5, 2021

Use GraphServiceClient with HttpClient in muti-thread apps

If you use GraphServiceClient from Graph SDK for .Net in multi-thread app consider reusing it as static instance instead of creating own instances inside each thread. GraphServiceClient itself is thread safe (see here) and can be reused within multiple threads as is (without locks, synchronizations, etc).

Otherwise it will internally create multiple HttpClient instances and you may face with reaching sockets limit. Alternatively you may create static HttpClient and then pass it to the GraphServiceClient instances created per thread as it has special constructor for that:

public GraphServiceClient(HttpClient httpClient)
  : base("https://graph.microsoft.com/v1.0", httpClient)
{
}

If you create threads intensively there may be too many instances of HttpClient created and you will reach sockets limit. E.g. this is how it looked in Azure function app with queue-triggered Azure function when GraphServiceClient instances were created inside function call (own instance per thread):


I.e. there were 2K connection peaks and the following errors in the logs:

An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full

When I changed the code and reused static instance of GraphServiceClient connections count got stabilized:



Friday, January 29, 2021

Calculate Sql Server database size having only db_reader permissions on target database

There are several possible ways to calculate database size (the same size which is shown when you right click on the database in Sql Server Management Studio > Properties > Files):

In this example we have 23 Mb of database file and 11 Mb of transaction log, total 34 Mb.

One way we can try is to run the following query:

select db_name(database_id) as database_name, 
    type_desc, 
    name, 
    size/128.0 as CurrentSizeMB
from sys.master_files
WHERE DB_NAME(database_id) = 'MyDatabase'

If you have permissions to run this command result will look like this:

As you can see it shows the same numbers as properties window shown above. The problem is that according to documentation you should have quite high server-level permissions to access sys.master_files:

The minimum permissions that are required to see the corresponding row are CREATE DATABASE, ALTER ANY DATABASE, or VIEW ANY DEFINITION.

If you don’t have one of these permissions result will be empty.

Is it possible to calculate database size having less permissions? E.g. having only db_reader permissions on target database. The answer is yes it is possible. In order to do that we need to use system stored procedure sp_spaceused:

use MyDatabaseName
exec sp_spaceused

It works also with db_reader permissions on the target database. Result will look like this:

It returns 2 result sets and in 1st result set it returns overall database size which is sum of db file name and transaction log (34 Mb in our example).

Tuesday, January 26, 2021

Authenticate in Sharepoint on-prem FBA site via OTB /_vti_bin/Authentication.asmx web service

Sharepoint contains number of OTB web services which are located inside _vti_bin virtual directory. In this article we will check one of them: authentication.asmx. This is quite interesting web service which allows to authenticate your app in Sharepoint FBA site. I.e. allows to send username and password and get FedAuth authentication cookies if provided credentials are valid (the same cookies which are used by Sharepoint FBA site when you successfully logged in via it’s login page).

Note that you may use this web service in server-side apps or mobile apps but not in client side JavaScript-based solution because FedAuth cookies have HttpOnly flag and thus can’t be used in JavaScript.

In order to use this web service we need to send special SOAP body in HTTP POST request. In order to get example of this SOAP open http://example.com/_vti_bin/authentication.asmx in browser (where instead of http://example.com you should use url of your site). It will show list of available web methods:

  • Login
  • Mode

Click on Login and you will get needed SOAP examples (in this article we will use SOAP 1.1):

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <Login xmlns="http://schemas.microsoft.com/sharepoint/soap/">
      <username>string</username>
      <password>string</password>
    </Login>
  </soap:Body>
</soap:Envelope>

In this SOAP body we need to set actual username and password in appropriate xml tags.

Here is example which shows how to send this SOAP body to authentication.asmx web service and get authentication cookies:

var cookies = new CookieContainer();
var handler = new HttpClientHandler();
handler.CookieContainer = cookies;
using (var httpClient = new HttpClient(handler))
{
    string soapBody =
    "<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>" +
    "  <soap:Body>" +
    "    <Login xmlns='http://schemas.microsoft.com/sharepoint/soap/'>" +
    "      <username>" + username + "</username>" +
    "      <password>" + password + "</password>" +
    "    </Login>" +
    "  </soap:Body>" +
    "</soap:Envelope>";
    var req = new HttpRequestMessage(HttpMethod.Post, "http://example.com/_vti_bin/authentication.asmx");
    req.Content = new StringContent(soapBody, Encoding.UTF8, "text/xml")
    var res = httpClient.SendAsync(req).GetAwaiter().GetResult();
    if (res.StatusCode == HttpStatusCode.OK)
    {
        var response = res.Content.ReadAsStringAsync().Result;
        var cookie = cookies.GetCookies(new Uri("http://example.com")).Cast<Cookie>().FirstOrDefault(c => c.Name == "FedAuth");
    }
}

In this example we first create HTTP POST request to authentication.asmx using SOAP body retrieved above and then if authentication was successful read FedAuth cookies returned from server. After that we may use these cookies for making authenticated calls to other web services. In one of the future articles I will show how to do that.

Monday, January 25, 2021

Get access token for calling AAD secured Azure functions via PowerShell

Some time ago I posted PowerShell example which lists all Azure AD groups via Rest API: List Azure AD groups via Rest Graph API in Powershell. However if you have custom Azure functions app which is secured via AAD (see Call Azure AD secured Azure functions from C#) you may use similar approach to get access token based on client id/secret (i.e. which will use app permissions) for calling your Azure functions from PowerShell or from 3rd party clients like Postman.

In order to get access token for calling AAD secured Azure functions you may use the following PowerShell script:

param
(
	[Parameter(Mandatory=$true)]
	[string]$Tenant,
	[Parameter(Mandatory=$true)]
	[string]$ClientId,
	[Parameter(Mandatory=$true)]
	[string]$ClientSecret
)

$currentDir = $PSScriptRoot
$dllCommonDir = resolve-path($currentDir + "\Common\")

[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($dllCommonDir, "Microsoft.Identity.Client.dll"))

function GetAccessToken($tenant, $clientId, $ClientSecret)
{
	$appCredentials = New-Object Microsoft.Identity.Client.ClientCredential -ArgumentList $ClientSecret
	$aadLoginUri = New-Object System.Uri -ArgumentList "https://login.microsoftonline.com/"
	$authorityUri = New-Object System.Uri -ArgumentList $aadLoginUri, $Tenant
	$authority = $authorityUri.AbsoluteUri
	$redirectUri = "urn:ietf:wg:oauth:2.0:oob"
	$clientApplication = New-Object Microsoft.Identity.Client.ConfidentialClientApplication($ClientId, $authority, $redirectUri, $appCredentials, $null, $null)
	[string[]]$defaultScope = @("https://graph.microsoft.com/.default")
	$authenticationResult = $clientApplication.AcquireTokenForClientAsync($defaultScope).Result
	return $authenticationResult.AccessToken
}

Write-Host "Acquiring access token..." -foregroundcolor green
$accessToken = GetAccessToken $Tenant $ClientId $ClientSecret
if ([System.String]::IsNullOrEmpty($accessToken))
{
	Write-Host "Can't acquire access token" -foregroundcolor red
	return
}
Write-Host "Access token successfully acquired" -foregroundcolor green
$filePath = [System.IO.Path]::Combine($currentDir, [System.String]::Format("_accesstoken_{0}.txt", [System.DateTime]::Now.ToString("yyyyMMddHHmmss")))
($accessToken) | Out-File $filePath -Append

It will fetch access token and will log it to text file. For calling this script you will need Microsoft.Identity.Client.dll assembly located in Common subfolder.

Saturday, January 9, 2021

One way to fix Failed to run web job error on Azure App Service

If you have just updated web job (see the following related articles: Upload WebJob to Azure App service with predefined schedule and How to remove Azure web job via Azure RM PowerShell) you may face with the following issue: when you will try to run updated web job you will get the error “Failed to run web job”. The problem is that error notification won’t contain any description and it will be hard to find the actual reason of this problem. In this article I will describe one of the reasons.

When web job is started it creates special lock file triggeredJob.lock in /data/jobs/triggered/{webJobName} folder of SCM site. You may check it if will connect to the site via FTP:

In the same folder there will be sub folders with timestamps which correspond to webjob runs. In these sub folders you will find output logs of each web job run – the same logs which are shown in Azure portal > App service > Web jobs > Logs. The purpose of this triggeredJob.lock file is to prevent launch of second instance of web job when previous instance didn’t finish yet. And the problem is that if during update web job was running this lock file may not be successfully deleted. As result when you will try to run updated version of web job you will get “Failed to run web job” error.

Solution will be to delete this file manually. However it is also not that straightforward. If you will try to remove it from FTP client you will get error

“The process cannot access the file because it is being used by another process”:

In order to delete it we need to stop both App Service and SCM site. Note that it is mandatory to stop both sites – if you will only stop App Service from Azure portal triggeredJob.lock will be still used by the process. Stopping of SCM site is more tricky than App Service. The process is described here: Full stopping a Web App. You need to go to Resource Explorer (azure.com) and select your App service. After that in JSON view on the right side click Edit and do 2 things:

  1. Change “state” from “Running” to “Stopped” – this is the same as if you would stop App service form Azure portal
  2. Find “scmSiteAlsoStopped” property and set it from “false” to “true” – it will stop SCM file

After that click PUT button on the top. It will stop both sites and you will be able to delete triggeredJob.lock now. Then go to Azure portal and start App service – it will start both sites. After all these steps you should be able to run web job again.