MS Graph - 401 Unauthorized seemingly with proper token and access - c#

I am working with an ASP.NET Core 2.0 application hosted on Azure and authenticates users through Microsoft using MSAL. I am getting the basic information through the authentication process like name, username and group claims. However, I want to access some additional information through MS Graph, like the users profile photo. Initial authentication and token acquisition runs smoothly, and sending a request to https://graph.microsoft.com/beta/me returns 200 OK. When trying to call https://graph.microsoft.com/beta/me/photo/$value, however, I get a 401 - Unauthorized in return.
I have seen several other posts on this issue, but most of them concludes that the developer have either forgotten to ask for the proper consents, gotten tokens from the wrong endpoints, or similar issues. All of which I have confirmed not to be the case.
I have confirmed that the proper scopes are included in the token using https://jwt.ms/. I also tried asking for greater scopes than necessary. Currently I am using the following scopes: openid profile User.ReadBasic.All User.Read.All User.ReadWrite Files.ReadWrite.All. According to the beta reference for get user the least required permission is User.Read and according to the reference for get photo the least required permission is also User.Read. Using the Graph Explorer I have also confirmed that I should have had access to the photo using the permissions that I do, although, I have not set any pictures on my profile so it gives me a 404 response.
I am at a loss as to why I cannot get access to the profile photo so any suggestions are much appreciated. If you need more information or details, please let me know. If relevant, I have a custom middleware that handles the post-authentication process of reading the user information which also makes the additional call to MS Graph for the photo.
Edit:
I also tried https://graph.microsoft.com/beta/users/{my-user-id}/photo/$value which yielded the same results - 404 in Graph Explorer and 401 through my code
Edit 2: Code
Here is the code that I am using. This first snippet is in a middleware that puts the claims from the authenticated user in a specific format. I have just been putting a break point on the return and inspected the response object.
public async Task GetUserPhotoAsync(string userid, HttpContext context)
{
HttpClient client = new HttpClient();
//client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var result = await new TokenHelper(_settings).GetAuthenticationAsync(userid, context, new string[] { "User.ReadBasic.All", "User.Read.All", "User.ReadWrite", "Files.ReadWrite.All" });
var url = "https://graph.microsoft.com/beta/me/photo/$value";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
return;
}
Here is the function that gets the token from the cache. MSALSessionCache is some code I have borrowed from here with some tweaks to fit .net core.
public async Task<AuthenticationResult> GetAuthenticationAsync(string signedInUserId, HttpContext context, string[] scopes)
{
TokenCache userTokenCache = new MSALSessionCache(signedInUserId, context).GetMsalCacheInstance();
ConfidentialClientApplication cca =
new ConfidentialClientApplication(_settings.ClientId, $"{_settings.Domain}/{_settings.AADInstance}/v2.0", "http://localhost:5000", new ClientCredential(_settings.ClientSecret), userTokenCache, null);
if (cca.Users.Count() > 0)
{
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes, cca.Users.First());
return result;
}
else
{
throw new Exception();
}
}
Initial token acquisition
options.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async context =>
{
string signedInUserId = context.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserId, context.HttpContext).GetMsalCacheInstance();
ConfidentialClientApplication cca =
new ConfidentialClientApplication(aadOptions.ClientId, aadOptions.RedirectUri, new ClientCredential(aadOptions.ClientSecret), userTokenCache, null);
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(context.ProtocolMessage.Code, new string[] { "User.ReadBasic.All", "User.Read.All", "User.ReadWrite", "Files.ReadWrite.All" });
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
Edit 3: Using the /v1.0 endpoint
As per Marc LaFleur's request I have tried the v1.0 endpoint with the same result. https://graph.microsoft.com/v1.0/me gives a 200 OK response code while https://graph.microsoft.com/v1.0/me/photo/$value returns 401 Unauthorized

I had the same problem with the Microsoft Graph giving a 401 Unauthorized exception when I was trying to query a user's photo or the photo's metadata on both the /V1.0 and /beta API endpoints. Like you, I verified I had the right tokens and was able to successfully access the user profile API.
In my case, I found it was because the photo for the user I was testing with hadn't been set. Once I assigned a photo I was able to successfully call both the photo value and photo metadata beta endpoints.
The v1.0 endpoints still gave me a 401 Unauthorized exception, but in my application I only use AzureAD, not Exchange. Based on #MarcLaFleur comments and the API documentation, this sounds like "expected" behaviour.
Why it returns a 401 Unauthorized instead of something like a 404 Not Found, or returning null values, I don't know.

Related

Unable to sign in to login.microsoftonline.com oauth 2 authorize endpoint

I've tried different ways to connect the Microsoft sign in function which open a webpage so you can use things like sign in with MFA. I manage to get this to work in Postman and now im trying it in C# particularly in .NET MVC 5.
HomeController:
public ActionResult TestAuth()
{
HttpClient client = new HttpClient();
var bodyParams = new Dictionary<string, string>();
bodyParams.Add("client_id", "{my_client_id}");
bodyParams.Add("client_secret", "{my_client_secret}");
bodyParams.Add("scope", "openid");
bodyParams.Add("redirect_uri", "https://localhost");
bodyParams.Add("grant_type", "authorization_code");
var response = client.PostAsync("https://login.microsoftonline.com/{my_tenant_id}/oauth2/v2.0/authorize", new FormUrlEncodedContent(bodyParams)).Result;
return View("TestAuth", new { response.Content.ReadAsStringAsync().Result });
}
View TestAuth.cshtml:
#model dynamic
#Html.Raw(Model)
If i sign in with my email on that domain, or any text at all really, i get this message. I cannot see why this issue occurs it gives me zero information what to do next more than just trying until you make it basically :). I've looked at tons of different Microsoft documentations, Stack posts, forums etc but with no success.
The postman call example:
Is it possible I'm doing something wrong in the request in the c# code or am i missing something important like configurations in Azure AD etc?
I'm up for anything that will work that i can sign into a Microsoft account that use MFA, then i can use their login to fetch data from Microsoft Graph based on their permissions basically.
P.S. I also can fetch data with the access token generated from postman so it's working as expected. I only need to "convert the postman call to c#" to make it work esentially. Any help is appreciated :)
You’re trying to do an oauth2 request from the controller. The request you’re sending is incorrect.
Microsoft made a great sample on how to use the Microsoft identity platform in a dotnet application https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/1-WebApp-OIDC
In a nutshell you redirect the user to the endpoint (so not a get/post from the controller, but actually a redirect 302 response to the token url).
The user then has to login and is redirected to the webapplication.
Your webapplication will get an authorization code that is has to exchange for an access token by a post request.
Postman does this for you, but in order to do it in dotnet core, just follow the sample.
I didn't find a soultion to this specific problem what i did find was another guide which led me to this github project https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect
Which had similar code in the Startup.cs file but actually had some examples like SendMail and ReadMail etc which was fetched from ms graph api. This gave me some idea of how this project was structured compared to mine. So one thing that was missing was this part I couldnt figure out:
IConfidentialClientApplication app = await MsalAppBuilder.BuildConfidentialClientApplication();
var account = await app.GetAccountAsync(ClaimsPrincipal.Current.GetAccountId());
So the Msal app builder which is a custom made thingy i needed to get the current user etc which i needed. This works fine and after that i can start doing requests to the graph api like adding scopes etc and make http request.
Example see groups:
[Authorize]
[HttpGet]
public async Task<ActionResult> GetGroups()
{
IConfidentialClientApplication app = await MsalAppBuilder.BuildConfidentialClientApplication();
var account = await app.GetAccountAsync(ClaimsPrincipal.Current.GetAccountId());
string[] scopes = { "GroupMember.Read.All", "Group.Read.All", "Group.ReadWrite.All", "Directory.Read.All", "Directory.AccessAsUser.All", "Directory.ReadWrite.All" };
AuthenticationResult result = null;
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/groups");
try
{
//Get acccess token before sending request
result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync().ConfigureAwait(false);
if (result != null)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
//Request to get groups
HttpResponseMessage response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
ViewBag.Groups= response.Content.ReadAsStringAsync().Result;
return View("MyView");
}
}
}
catch (Exception ex)
{
Response.Write($"Error occured:{System.Environment.NewLine}{ex}");
}
return View();
}

Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential

I have a console app written with C# on the top of .NET Core 2.2 framework.
I am trying to use my app to connect Google My Business API to create posts.
But every time I try to call the REST API I get the following error
Request had invalid authentication credentials. Expected OAuth 2
access token, login cookie or other valid authentication credential.
See
https://developers.google.com/identity/sign-in/web/devconsole-project.
The code worked previously, but for some odd reason, it stopped!
Hereis an example where I get authentication token and then call the API to get a list of Google accounts.
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets
{
ClientId = "Client ID",
ClientSecret = "Client Secret",
}, new[] { "https://www.googleapis.com/auth/plus.business.manage" }, "google username", CancellationToken.None);
using (var client = new HttpClient())
{
//client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", credential.Token.AccessToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var c = await client.GetAsync("https://mybusiness.googleapis.com/v4/accounts");
var accountContentss = await c.Content.ReadAsStringAsync();
c.EnsureSuccessStatusCode();
var accountContent = await c.Content.ReadAsStringAsync();
}
I am able to authenticate and get the access token with no issue. However, the second call to the API fails for some reason.
How can I correctly call Google API? Beside the AccesToken, is there something else that should get passed in the header?
It turned out to be that GoogleWebAuthorizationBroker.AuthorizeAsync() was returning an expired token! It would have been very helpful if the API returned a message stating that the token is expired instead!
To fix the issue, instead of accessing the AccessToken manually (i.e, credential.Token.AccessToken), I used await Credential.GetAccessTokenForRequestAsync() method to get an access token.
The Credential.GetAccessTokenForRequestAsync() method returns an valid token any time it is called. In other words, if the token is expired, it uses the refresh-token to generate a new one. Otherwise, it return the existing non-expired token.
I change my Authorization header to this
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await Credential.GetAccessTokenForRequestAsync());

How to acquire graph token without using sessions?

I have an application that is utilizing Azure AD authentication. I also need to access the Microsoft Graph API for user data. Every example I have found that makes requests to the Graph API is utilizing a cached session token, but since I am using JWT obviously I have no need for storing session state. How can I get a JWT with the proper audience using a JWT with my app as the audience?
For example, here is a request to retrieve a token from the Microsoft Graph AspNetCore Sample:
_userTokenCache = new SessionTokenCache(userId, _memoryCache).GetCacheInstance();
var cca = new ConfidentialClientApplication(
_appId,
_redirectUri,
_credential,
_userTokenCache,
null);
var result = await cca.AcquireTokenSilentAsync(_scopes, cca.Users.First());
return result.AccessToken;
Which utilizes the memory cache to pull the token from a Challenge() redirect sign-in with OpenId Connect cookie. However, since I am using JWT, I already have a bearer token, but with the wrong authority. What do I need to do to acquire a new token that I can use to access the Graph API? I still want the tokens to be authorized for my application id, so I would want a new token that allows me to access the API with server-side rest requests.
Edit: Incorrectly tagged as Azure AD Graph, retagged to Microsoft Graph.
Edit Edit: To clarify, each of the samples I've seen so far is using Session cookies as so:
services.AddAuthentication(sharedOptions => {
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();
However, I am using JWT so I don't have a token cached:
app.UseJwtBearerAuthentication(new JwtBearerOptions {
Authority = $"{instance}{tenant}",
Audience = audience,
SaveToken = true
});
The JWT that I get from requests to login.microsoftonline.com have my application as the audience, whereas the JWT generated by these samples have https://graph.microsoft.com as the audience. So I need to get (I presume at least) a token for this audience using only the token I got from my standard authentication request.
Don't confuse how you manage your token (i.e. token cache) with the tokens themselves. The reason you cache a token is simply so you can request a refreshed token as needed (refresh_token). The refresh token is only provided for certain sceanios (i.e. when using the authorization_code flow and you've requested the offline_access scope).
If you're using a flow without a refresh token (i.e implicit or client_credentials) then you may not need to cache your token. You generally should still cache them since there is an overhead cost to fetching a token from AAD and caching allows you to only retrieve a new token when the existing one expires.
Using DelegateAuthenticationProvider with an existing Token
All that said, it sounds like you've already got a token in hand. Since the entire point of MSAL (which is where ConfidentialClientApplication comes from) it to retrieve and manage tokens for you, I'm not exactly sure why you'd want to do this. I would simply skip MSAL entirely and just use your existing token.
If you're using the Microsoft Graph .NET Client Library you can drop MSAL entirely and simply use your existing token (access_token) via the DelegateAuthenticationProvider:
var graphServiceClient = new GraphServiceClient(
new DelegateAuthenticationProvider((requestMessage) => {
requestMessage.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", token.access_token);
return Task.FromResult(0);
})
);
As for the "proper audience", I'm not sure I understand the context. Your token will need to include scopes for Microsoft Graph but how you define them depends a bit on how you are getting your token.
v1 Endpoint
If you're using the older Azure AD OAUTH endpoint (aka the v1 Endpoint) then you need to configure your Application permissions via the Azure Portal. In order to switch between different APIs (called "Resources") you need to request offline_access and user the refresh_token. Switching involves requesting a refreshed token while passing in a new resource. The resulting token will then work with that resource.
For example, if my default resource is a SharePoint Online instance (https://tenant.sharepoint.com) then I would normally refresh my token with something like this:
private async Task<string> RequestTokenAsync() {
var data = new Dictionary<string, string>();
data.Add("grant_type", "refresh_token");
data.Add("client_id", _clientId);
data.Add("client_secret", _clientSecret);
data.Add("resource", "https://tenant.sharepoint.com");
data.Add("redirect_uri", RedirectUri);
data.Add("refresh_token ", refresh_token);
HttpClient httpClient = new HttpClient();
var response = await httpClient.PostAsync(_tokenUri, new FormUrlEncodedContent(data));
response.EnsureSuccessStatusCode();
var result = await result.Content.ReadAsStringAsync();
}
Now if I want to make a call to Microsoft Graph I will first need to get a token for the https://graph.microsoft.com resource:
private async Task<string> RequestTokenAsync() {
var data = new Dictionary<string, string>();
data.Add("grant_type", "refresh_token");
data.Add("client_id", _clientId);
data.Add("client_secret", _clientSecret);
data.Add("resource", "https://graph.microsoft.com");
data.Add("redirect_uri", RedirectUri);
data.Add("refresh_token ", refresh_token);
HttpClient httpClient = new HttpClient();
var response = await httpClient.PostAsync(_tokenUri, new FormUrlEncodedContent(data));
response.EnsureSuccessStatusCode();
var result = await result.Content.ReadAsStringAsync();
}
Now I have two tokens, one for SharePoint and one for Microsoft Graph. I can switch between resources by simply refreshing the token for the proper resource. I do have to make sure I refresh properly however since if my refresh_token expires before I can replace it, I've lost my credentials entirely.
If this sounds complicated, it is. Generally you need to build some mechanisms to manage which tokens are live, which tokens need to be replaced, etc. This is what that token cache is all about since MSAL/ADAL handle this for you.
v2 Endpoint
The newer v2 Endpoint is far easier to work with. Rather than resources it uses scopes. These scopes include the resource identifier and can be dynamically assigned as needed.
So while in v1 we might assign user.read from Microsoft Graph and user.read from Outlook Rest API, we can now assign both at once in a single token by requesting https://graph.microsoft.com/user.read and https://outlook.office.com/user.read at the same time. This means we get a single token that can be used with either API without getting into the "refresh to switch resource" business from above.
The downside of v2 is that only a limited number of APIs support it at the moment. If you need to work across a number of APIs, you may still be better off using v1 for this reason.
Hope this helps a little.

Microsoft Graph API Update another user's photo?

Using the Microsoft Graph API I was able to get a list of all users in our Azure Active Directory tenant and determine if they have a profile picture. I wanted to then take the list of users without a photo and upload one for them, but the API returns a 403 error even though the account I'm using has full access to all of the user accounts and the application is setup with full permissions to the Graph API.
using (HttpClient client = new HttpClient())
{
client.BaseAddress = new Uri("https://graph.microsoft.com/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("image/jpeg"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", oauthToken);
// HTTP GET
HttpResponseMessage response = await client.PatchAsync($"v1.0/users/{emailAddress}/photo/$value", byteContent);
if (!response.IsSuccessStatusCode)
{
throw new Exception("Error!");
}
}
403 FORBIDDEN
Is it not possible to do this using the Graph API or am I missing a permission somewhere?
The SCP value is:
Calendars.Read Calendars.ReadWrite Contacts.Read Contacts.ReadWrite Directory.AccessAsUser.All Directory.Read.All Directory.ReadWrite.All email Exchange.Manage Files.Read Files.Read.Selected Files.ReadWrite Files.ReadWrite.AppFolder Files.ReadWrite.Selected full_access_as_user Group.Read.All Group.ReadWrite.All Mail.Read Mail.ReadWrite Mail.Send MailboxSettings.ReadWrite Notes.Create Notes.Read Notes.Read.All Notes.ReadWrite Notes.ReadWrite.All Notes.ReadWrite.CreatedByApp offline_access openid People.Read People.ReadWrite profile Sites.Read.All Tasks.Read Tasks.ReadWrite User.Read User.Read.All User.ReadBasic.All User.ReadWrite User.ReadWrite.All
First of all, you should take a look at the new Microsoft Graph SDK, released during //Build 2016
Here is the Github of Microsoft Graph SDK : https://github.com/microsoftgraph
Here is a full sample I ve created, using it :
https://github.com/Mimetis/NextMeetingsForGraphSample
For your question, here are two methods I've written, which worked for me :
I assume, you have a method to get a valid access token.
using (HttpClient client = new HttpClient())
{
var authResult = await AuthenticationHelper.Current.GetAccessTokenAsync();
if (authResult.Status != AuthenticationStatus.Success)
return;
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + authResult.AccessToken);
Uri userPhotoEndpoint = new Uri(AuthenticationHelper.GraphEndpointId + "users/" + userIdentifier + "/Photo/$value");
StreamContent content = new StreamContent(image);
content.Headers.Add("Content-Type", "application/octet-stream");
using (HttpResponseMessage response = await client.PutAsync(userPhotoEndpoint, content))
{
response.EnsureSuccessStatusCode();
}
}
If you use the Microsoft Graph SDK, it will be really straightforward :)
GraphServiceClient graphService = new GraphServiceClient(AuthenticationHelper.Current);
var photoStream = await graphService.Users[userIdentifier].Photo.Content.Request().PutAsync(image); //users/{1}/photo/$value
Seb
The documentation is pretty clear about updating a photo of an other user.
To update the photo of any user in the organization, your app must have the User.ReadWrite.All application permission and call this API under its own identity, not on behalf of a user. To learn more, see get access without a signed-in user.
This means you'll need an access token for the application, and not the token you get if the user accesses your application. This page shows how to get an application access token. This also means you have to grant your application the application permissions, which is often forgotten.
Once you get the token you can check it on https://jwt.ms to see the claims in the token explained.
You're talking about the scp claim in the token. That is only there if you have a user token, not an application token.

AdminSettings API using service account in a C# Console application

I'm trying to use the Google Admin Settings API with a Service Account with no success from a C# Console application.
From what I've understood, I first have to get an OAuth token. I've tried 2 methods successfully for this: using Google.Apis.Auth.OAuth2.ServiceAccountCredentials or by creating manually the JWT assertion.
But when I call an Admin Settings API with the OAuth token (maximumNumberOfUsers for instance), I always get a 403 error with " You are not authorized to perform operations on the domain xxx" message.
I downloaded GAM as the author calls this API too so that I can compose the same HTTP requests. Like explained in GAM wiki, I followed all the steps to create a new Service Account and a new OAuth Client ID so that I can be sure it's not a scope issue. I also activated the debug mode like proposed by Jay Lee in this thread. Like explained in the thread comments, it still doesn't work with my OAuth token but the call to the API succeeds with GAM OAuth token.
So it seems it's related to the OAuth token itself. An issue I get while creating the OAuth token is that I can't specify the "sub" property (or User for ServiceAccountCredentials). If I add it, I get a 403 Forbidden response with "Requested client not authorized." as error_description while generating the token i.e. before calling the API. So maybe it is the issue but I don't see how to fix it as I use an Admin email.
Another possibility is that this API needs the OAuth Client credentials as GAM requires 2 different types of credentials, Service Account and OAuth Client. As I only can use Service Account credentials in my project, I'm afraid I will be stuck if it is the case...
I don't see other options and I'm stuck with both, so any help appreciated. Thanks!
My code:
public static string GetEnterpriseUsersCount()
{
string domain = MYDOMAIN;
string certPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
certPath = certPath.Substring(0, certPath.LastIndexOf("\\") + 1) + "GAMCreds.p12";
var certData = File.ReadAllBytes(certPath);
X509Certificate2 privateCertificate = new X509Certificate2(certData, "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(SERVICE_ACCOUNT_EMAIL)
{
Scopes = new[] { "https://apps-apis.google.com/a/feeds/domain/" },
User = ADMIN_EMAIL
}.FromCertificate(privateCertificate));
Task<bool> oAuthRequest = credential.RequestAccessTokenAsync(new CancellationToken());
oAuthRequest.Wait();
string uri = string.Format("https://apps-apis.google.com/a/feeds/domain/2.0/{0}/general/maximumNumberOfUsers", domain);
HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest;
if (request != null)
{
request.Method = "GET";
request.Headers.Add("Authorization", string.Format("Bearer {0}", credential.Token.AccessToken));
// Return the response
using (WebResponse response = request.GetResponse())
{
using (StreamReader sr = new StreamReader(response.GetResponseStream()))
{
return sr.ReadToEnd();
}
}
}
return null;
}
Edit: I focused on scopes like advised by Jay Lee below and it appears that the missing scope was 'https://www.googleapis.com/auth/admin.directory.domain'. However, nowhere is this written in Admin Settings API documentation page. At least, I didn't find it. 'https://apps-apis.google.com/a/feeds/domain/' is necessary too but I already added it to the list of allowed scopes. Thanks Jay!
Edit 2: I also updated the source code so that it can help in the future.
You need to grant your service account's client ID access to the scopes for admins settings API. Follow the Drive domain wide delegation instructions except sub in the correct correct scope. Then you can set sub= without an error.

Categories

Resources