Adidas Manchester Marathon 2025: second marathon and sub 3:30 achieved

This was my second marathon. The first was in October last year and went super well; I targeted 3:45, used the Pfitzinger/Douglas (Pfitz) 18/55 schedule (in the book Advanced Marathoning) for training, and while it was fatiguing I finished in 3:37 and (as I now know) in relatively good shape.

This time around I wanted to beat 3:30 and repeated Pfitz 18/55 but with faster pace. Training went well overall, no major injuries, and I mostly hit the workout paces though I find what he calls the V̇O2 Max sessions difficult to achieve; these are the most demanding sessions intended to improve one’s maximum rate of oxygen consumption during running.

Before the race

Got to Manchester Saturday afternoon, didn’t get to run at all though Pfitz schedules a 4 mile recovery run saying it is mainly there to keep you from fretting. Difficult to carb load when travelling but I did my best with a big bowl of cereal for breakfast followed by a bacon roll later, packed lunch with chunky cheese sandwiches and fresh fruit, then I consumed a mid-afternoon Ploughmans with a fruit smoothie and in the evening, a delicious goats cheese flatbread. I also drank loads of water, not sure exactly how much but several pints during the day. I think this did help as I was well hydrated before the race.

Set the alarm for 5:30am not that I needed to; didn’t sleep brilliantly. Cup of tea and sourdough roll early (Pfitz suggests eating 3-4 hours before running), more water. Then I headed for the start on the first tram from my hotel at about 6:25; no reason to be so early except that I didn’t have anything else to do! Always enjoy chatting with other runners and their supporters.

My first marathon had fewer than 1000 runners, this one claimed 36,000 though I am sure the actual number was a bit less. All a bit different though. Arrived Old Trafford tram stop, 10 minute walk to the start area. Dropped my bag at around 8:00am and was a little cold in my club vest, did some half-hearted short warm-up runs and took advantage of the portaloos. This was a success actually, the benefit of the early start meant that I had plenty of time to get ready as it were and had zero GI issues during the race.

I was in bronze wave which was the fifth to start after Elite, White, Red and Blue. Each wave had I gather up to 1800 runners which is biggish and I found myself towards the back of the wave for some reason. This didn’t matter as such, but the 3:30 pacers were towards the front and it wasn’t possible therefore to start with them. More on this later.

The Race

Off we go and I am trying to go no faster then a 7:45 pace (all my figures are in miles) and no slower than 8:00. Some congestion but it went fairly well and the miles started ticking by. Took an energy gel at the start and another 4 miles in. Skipped the first water station, grabbed a bottle at the second, but found drinking quite hard while running, I have learned that I can’t take a big gulp as it can easily turn into a coughing fit. Did my best; at least with the bottles you can carry them for a bit and take occasional sips.

After maybe 6 miles (can’t remember exactly), I catch up with a 3:30 pacer (there were two) and had a chat. Now, I knew from my Garmin that I was on target for sub-3:30 but of course having started at the back of the wave the pacers were a little in front. It was tricky though; the guy said he was currently running ahead of pace because he had a bathroom break and was now catching up with the other pacer. So I didn’t want to keep pace with him as it would be too fast. Off he went into the distance.

At the half-way point I finally caught up with the other 3:30 pacer. This was odd because he was now quite a way behind first guy. I asked about this and he said his fellow pacer was more than a minute ahead of time. He also said he was a bit ahead, and his plan was to to slow down at 26 miles and then wave people past to get their time. I concluded that my best strategy was to try and stay ahead of him.

I was more fatigued at this point than I was when half-way in October but there were two good reasons for that. One was that my pace was about 7:54/mile versus 8:16 or so in October. The other was the heat; we were running in up to 20 degrees Celsius and bright sunshine whereas October was overcast with occasional light rain and much cooler (I don’t know the exact temperature).

Another thought: for this event I trained in the cool of the winter and then ran in relatively warm conditions; where last time I trained in the heat of the summer and then ran in relatively cool conditions – an easier transition!

Anyway I was keen to get to 20 miles as Pfitz calls 13-20 the “no-mans land of the marathon” when you can easily lose pace. Kept the pace fairly well and was helped by the pacer because if he appeared beside me I knew I had to run faster!

Got to 20 miles and by this time I was seriously fatigued. I took my first carb-packed SIS beta fuel at this point and told myself it would help me to keep going.

The last 6 miles were tough. I can’t say I hit the wall; I lost a little pace but it wasn’t terrible – my slowest mile according to Garmin was mile 24 at 8:04 pace. Heat was getting to me, really needed water at the last drink station. It was difficult to drink enough, but poured the rest over my head, great idea and I should have done this before.

It is hard to describe the mental battle that was taking place in those last miles. I told myself to just keep running even though I no longer had the energy for it. I told myself to run fast so that the ordeal would be over quicker. I thought of my family and friends tracking me and did not want to disappoint them. And again, if the 3:30 pacer appeared, I did my best to run faster (and in the end, I did finish ahead of him, but a few seconds behind the other 3:30 pacer).

The crowd at Manchester was fantastic and it was great to hear all the shouts of encouragement including my name from time to time (name were shown on the bibs). I also passed plenty of runners walking and told myself that I would not do that. Just one parkrun to go. Just two miles to go. Then there is a sign that says you are on the final straight, most welcome but I did not have it in me to up the pace. Kept running, tried to smile for the camera, crossed the line, and then there was a weird moment: is it really OK to slow down now?

I was pretty sure that I had beaten 3:30 but did not get the exact time until later (not carrying a phone). 3:27:46, a personal best by over 9 minutes and a good for age for London next year I hope, with over 24 minutes below the required time (V65 M).

After the ordeal

Finishing a marathon can be anti-climactic. I felt good about my time but also very fatigued, and the first thing you do after finishing at a big event like this is quite a lot of walking, since the organisers have to keep the finish area clear. So picked up a bottle of water (good thinking!), walked to medal area, picked up tasty non-alcoholic tin of beer (chilled! Nice), walked to picked up bag, quite a long way with several lorries for each start wave, then walked to T-shirt area, then you come out of the finish area and eventually to a sign that says Piccadilly Station 14 mins walk, my hotel was near there so I just plodded on, eventually sitting down to a lovely latish lunch there with a cold beer and some fellow finishers.

I can’t really fault the organization, the start was smooth and on time, the facilities were good, the medal is nice, all very professional, but there is inevitably a lot of standing around at the start and walking at the end.

The pacers worked hard but were not ideal for me. I would prefer that they ran closer together and with even splits, though they did finish pretty much on the button.

While I was happy with my time it was actually slower than the 3:19 VDOT predicted from my best half in February; I think this shows the impact of the heat as my effort was as great or greater.

Despite piling on the suncream I caught the sun in a few spots, my advice if you are running this summer is don’t forget the back of your legs and all round your arms as that is where I went wrong. Wore a cap which I do think helped.

Finally, kudos to the Pfitz plan and book which got me through again without any calamities, such a great resource.

Creating a secure ASP.Net Core web application with Entra ID (formerly Azure AD) auth by group membership – harder than it should be

Several years ago I created a web application using ASP.NET and Azure AD authentication. The requirement was that only members of certain security groups could access the application, and the level of access varied according to the group membership. Pretty standard one would think.

The application has been running well but stopped working – because it used ADAL (Azure Active Directory Authentication Library) and the Microsoft Graph API with the old graph.windows.net URL both of which are deprecated.

No problem, I thought, I’ll quickly run up a new application using the latest libraries and port the code across. Easier than trying to re-plumb the existing app because this identity stuff is so fiddly.

Latest Visual Studio 2022 17.13.6. Create new application and choose ASP.NET Core Web App (Razor Pages) which is perhaps the primary .NET app framework.

Next the wizard tells me I need to install the dotnet msidentity tool – a dedicated tool for configuring ASP.NET projects to use the Microsoft identity platform. OK.

I have to sign in to my Azure tenancy (expected) and register the app. Here I can see existing registrations or create a new one. I create a new one:

I continue in the wizard but it errors:

This does not appear to be an easy fix. I click Back and ask the wizard just to update the project code. I will add packages and do other configuration manually. Though as it turned out the failed step had actually added packages and the app does already work. However Visual Studio is warning me that the version of Microsoft.Identity.Web installed has a critical security vulnerability. I edit Nuget packages and update to version 3.8.3.

The app works and I can sign in but it is necessary to take a close look at the app registration. By default my app allows anyone with any Entra ID or personal Microsoft account to sign in. I feel this is unlikely to be what many devs intend and that the default should be more restricted. What you have to do (if this is not what you want) is to head to the Azure portal, Entra ID, App registrations, find your app, and edit the manifest. I edited the signInAudience from AzureADandPersonalMicrosoftAccount to be AzureADMyOrg:

noting that Microsoft has not been able to eliminate AzureAD from its code despite the probably misguided rename to Entra ID.

However my application has no concept of restriction by security group. I’ve added a group called AccessITWritingApp and made myself a member, but getting the app to read this turns out to be awkward. There are a couple of things to be done.

First, while we are in the App Registration, find the bit that says Token Configuration and click Edit Groups Claim. This will instruct Azure to send group membership with the access token so our app can read it. Here we have a difficult decision.

If we choose all security groups, this will send all groups with the token including users who are in a group within a group – but only up to a limit of somewhere between 6 and 200. If we choose Groups assigned to the application we can limit this to just AccessITWritingApp but this will only work for direct members. By the way, you will have to assign the group to the app in Enterprise applications in the Azure portal but the app might not appear there. You can do this though via the Overview in the App registration and clicking the link for Managed application in local directory. Why two sections for app registrations? Why is the app both in and not in Enterprise applications? I am sure it makes sense to someone.

In the enterprise application you can click Assign users and groups and add the AccessITWritingWebApp group – though only if you have a premium “Active Directory plan level” meaning a premium Entra ID directory plan level. There is some confusion about this.

Another option is App Roles. You can assign App Roles to a user of the application with a standard (P1) Entra ID subscription. Information on using App Roles rather than groups, or alongside them, is here. Though note:

“Currently, if you add a service principal to a group, and then assign an app role to that group, Microsoft Entra ID doesn’t add the roles claim to tokens it issues.”

Note that assigning a group or a user here will not by default either allow or prevent access for other users. It does link the user or group with the application and makes it visible to them. If you want to restrict access for a user you can do it by checking the Assignment required option in the enterprise application properties. That is not super clear either. Read up on the details here and note once again that nested group memberships are not supported “at this time” which is a bit rubbish.

OK, so we are going down the groups route. What I want to do is to use ASP.NET Core role-based authorization. I create a new Razor page called SecurePage and at the top of the code-behind class I stick this attribute:

[Authorize(Roles = "AccessITWritingApp,[yourGroupID")]
public class SecurePageModel : PageModel

Notice I am using the GroupID alongside the group name as that seems to be what arrives in the token.

Now I run the app, I can sign in, but when I try to access SecurePage I get Access Denied.

We have to make some changes for this to work. First, add a Groups section to appsettings.json like this:

"Groups": {
"AccessItWritingApp": "[yourGroupIDhere]"
},

Next, in Program.cs, find the bit that says:

// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

and change it to:

// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{ 
// Ensure default token validation is carried out
builder.Configuration.Bind("AzureAd", options);
// The following lines code instruct the asp.net core middleware to use the data in the "roles" claim in the [Authorize] attribute, policy.RequireRole() and User.IsInRole()
// See https://docs.microsoft.com/aspnet/core/security/authorization/roles for more info.
options.TokenValidationParameters.RoleClaimType = "groups";
options.Events.OnTokenValidated = async context =>
{
if (context != null)
{
List requiredGroupsIds = builder.Configuration.GetSection("Groups")
.AsEnumerable().Select(x => x.Value).Where(x => x != null).ToList();
// Calls method to process groups overage claim (before policy checks kick-in)
//await GraphHelper.ProcessAnyGroupsOverage(context, requiredGroupsIds, cacheSettings);
    }
    await Task.CompletedTask;
};
}
);

Run the app, and now I can access SecurePage:

There are a few things to add though. Note I have commented a call to GraphHelper; you might want to uncomment this but there are further steps if you do. GraphHelper is custom code in this sample https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/ and specifically the one in 5-WebApp-AuthZ/g-2-Groups. I do not think I could have got this working without this sample.

The sample does something clever though. If the token does not supply all the groups of which the user is a member, it calls a method called ProcessAnyGroupsOverage which eventually calls graphClient.Me.GetMemberGroups to get all the groups of which the user is a member. As far as I can tell this does retrieve membership via nested groups though note there is a limit of 11,000 results.

Note that in the above I have not described how to install the GraphClient as there are a few complications, mainly regarding permissions.

It is all rather gnarly and I was surprised that years after I coded the first version of this application there is still no simple method such as graphClient.isMemberOf() that discovers if a user is a member of a specific group; or a simple way of doing this that supports nested groups which are often easier to manage than direct membership.

Further it is disappointing to get errors with Visual Studio templates that one would have thought are commonly used.

And another time perhaps I will describe the issues I had deploying the application to Azure App Service – yes, more errors despite a very simple application and using the latest Visual Studio wizard.