Invoke-RestMethod to a Microsoft Form

After the previous post I was curious if I could connect to a Microsoft Form via something like Invoke-RestMethod. You know, query it and get the Owner Id for instance.

This thought ate my head for a day as I tried various things unsuccessfully. It all boils down to my not having a real understanding of OAuth 2.0, but that’s fine I suppose as this is how I learn… I learn less from reading something, more from just brute force working my way through it and making mistakes and getting frustrated.

Anyways,this seems to be the flow of authentication when I visit a Forms page from my browser:

When you visit https://forms.microsoft.com/ it takes you to https://login.microsoftonline.com/ (via login.windows.net; not sure if that matters). This is Azure AD. That authenticates you and sends you back to Forms. This seems to repeat again, not sure why, but I am going to ignore that too.

The key thing is the client_id is c9a559d2-7aab-4f13-a6ed-e7e9c52aec87. That is Microsoft Forms in the tenant.

The request to Azure AD is something like this (adding line breaks):

What do we learn from this? The client_id and resource of course. The scopes (openid & profile). The type of flow – this is an Authorization Code Flow – based on the response_type being code and id_token (the former is the key thing). This returns an auth code, which has to be posted (response_mode=form_post) to a specified URL (redirect_uri=).

Can I try accessing this via a Device Code Flow instead? Something like this:

Open a browser and authenticate.

Then continue:

And it fails:

That error (The request body must contain the following parameter: ‘client_assertion’ or ‘client_secret’) indicates the App Registration is not setup for public flows. It’s this setting under the App Registration:

I can’t do anything about that as this is an App Registration in Microsoft’s tenant and what I have is the cross-tenant object.

So that’s out. So is the ROPC Flow then. Which didn’t make sense… I do access Forms by entering my username and password (and MFA when required) on the Azure AD login page.

How about if I try to authenticate against Graph and put the scope as the App Id of Microsoft Forms? Doesn’t make sense, but is similar to something I did earlier when I authenticated to an App Registration and put the scope as “api://<another app registration>“. So it’s basically similar PowerShell as above but with this instead:

As expected that failed: “The application ‘00000003-0000-0000-c000-000000000000’ asked for scope ‘c9a559d2-7aab-4f13-a6ed-e7e9c52aec87’ that doesn’t exist”. Of course.

Naturally, I am stupid enough to try with others too… like Azure AD (App Id 00000002-0000-0000-c000-000000000000) or PowerApps (App Id a7c47198-405d-42a7-ad00-3a0df33e10ad) and they all said the same. :)

So ROPC and Device Code Flows are definitely out. If those worked I could have done the entire thing via PowerShell itself, but no probs… this exercise is more for my curiosity than any practical reasons. Can I get the token using the Auth Code Flow that’s already being used, and put that into Invoke-RestMethod?

To recap, this is what happens initially:

If I put in that Url as is in a browser (where I am already authenticated) it simply takes me to https://forms.microsoft.com/pages/silentsignincomplete.aspx. That’s not done, I want the auth code to be given rather than sent to a page. So I changed the reponse_mode=form_post to response_mode=fragment. The latter tells Azure AD to return the code in the response Url itself.

If I paste the modified Url I get the following response (I added line breaks and removed the actual id_token and auth code):

Nice!

As an aside, I was curious about the stuff in the &state= part. This is stuff we send over in the initial request, so what is it? I put “eyJ2ZXJzaW9uIjoxLCJkYXRhIjp7IklkZW50aXR5UHJvdmlkZXIiOiJBU0tDVFB0VHVQcFpyYm5Rb1dtWVRBTVI0ME9GUEpXbXA1TEVuUnNncExCcG9EYVFtNU5BS1k3LTlqZFZxbldidHg0SFMwenFBT2dFQXprTEt3U0RSS3MiLCJwcm9tcHQiOiJBVDg4OHZfc3RGZzFrUksta0t3S2RJVkxaX3F6MjhDOF9ibUl2QkhBbGJabm1YdnpSUV81QVdhaklFRVRWdm01bl9qX1BKTE5hcHlJQ3drZGlPQzRLbkEiLCIucmVkaXJlY3QiOiIvUGFnZXMvU2lsZW50U2lnbkluQ29tcGxldGUuYXNweCJ9fQ” into a Base64 decoder and got:

Hmm, so there’s where Url I was sent to above comes from.

Anyways, armed with the code from above, the next step is to exchange that code for an access token.

And that didn’t work!

Seeing the error message, it makes sense (The request body must contain the following parameter: 'client_assertion' or 'client_secret'). With an Auth Code Flow when I want to authenticate with say Microsoft Forms, it sends me to the /authorize endpoint to authenticate with Azure AD. Once I do that, Azure AD gives me the auth code which is sent directly to an end point of the App (Microsoft Forms in this case, we saw that above). The App then takes this code and since it is already registered with Azure AD and has a client secret with Azure AD, it sends these two itself to Azure AD to get an access token… and that access token is returned to me. Subsequently I (or rather my browser) can use this access token to authenticate directly with the App.

Below if a picture from the official docs:

My mistake here was that when I intercept the code and send it to Azure AD to get a token, Azure AD goes “whoaaa! where’s the secret?” as that secret is how Azure AD can validate the code is being sent from the Forms App. This is a crucial step actually, and that’s what makes the Auth Code Flow special..

The key thing is the client_secret is stored in the App. So I or my browser don’t have access to it… Microsoft Forms has its own secret, so only it can send the code + secret to Azure AD. And since this happens from the Microsoft Forms servers to Azure AD, no can intercept it either.

All that’s great knowledge and am glad I figured it out, but where does that leave me? :) If I am using the Auth Code Flow that means I have to send the code to Microsoft Forms and it will do the needful and get back with the access token. So maybe I just take that from the browser?

So I went further down the authentication traffic I had captured earlier… and as you can see below I go from https://forms.microsoft.com/ => https://login.microsoftonline.com/ (Azure AD) => https://form.microsoft.com/landing (this was the redirect_uri) => https://forms.microsoft.com/Pages/SilentSignInComplete.aspx (the page I got taken to above when I put the initial Url into my browser the first time). It stands to reason that this final Url is when my browser gets the access token. I do see an AADAuth.forms= in the cookie, and that looks like an access token.

So I copy pasted that into PowerShell and tried:

And bingo! that worked. 🥳

(Actually, the first time I tried this, it didn’t work. Got the following error: code=701; message=Required user login.; @ms.form.error.type=ExpectedFailure

That stumped me for a bit until I realized the token probably expired. So I tried again, with a fresh token, and that worked!)

Sweet. Now I can easily get the Owner Id as before:

Not bad eh.

That got me thinking… Auth Code Flow is the newer way. Before that you had Implicit Grant, which is pretty much the same without the auth code steps. That is, you ask for the access token from the /authorize endpoint itself (rather than from the /token endpoint where you would exchange) and that’s it. It is not recommended to use the Implicit Grant Flow (screenshot from the previous link):

What if I do the Implicit Grant Flow here though? There’s no reason to do it as it is not recommended, but I am curious.

So I take the Url we used earlier (I added line breaks):

Changed response_mode=fragment as before, and response_type=code%20id_token to just response_type=id_token. Looks like this, without line breaks:

Pop that into a browser and once it signs in I am taken to (line breaks added):

And there we go – the access token. If I put that access token into the same PowerShell as above, that too works!

This entire excercise has no practical value as far as I know but was a good learning exercise and I am glad I cracked this. 🙂