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.
Related
I've got the following scenario (all applications and services are using asp.net core 3.0 on Windows):
I have a client application that runs in the context of AppUser. It uses an HttpClient to send a request to WebServiceA that runs in the context of WebUser.
The request uses Windows authentication.
var credentialsCache = new CredentialCache { { uri, "Negotiate", CredentialCache.DefaultNetworkCredentials } };
var handler = new HttpClientHandler { Credentials = credentialsCache };
var httpClient = new HttpClient(handler);
In WebServiceA, the call is recieved correctly and it's authenticated:
HttpContext.User.Identity.IsAuthenticated is true and HttpContext.User.Identity.Name is AppUser.
To be able to process the request, WebServiceA must send another request to WebServiceB. This request must also be authenticated as AppUser.
Currently I'm using the following code to create the HttpClient that makes the request to WebServiceB:
var credentialsCache = new CredentialCache { { uri, "Negotiate", CredentialCache.DefaultNetworkCredentials } };
HttpClientHandler handler = new HttpClientHandler
{
Credentials = credentialsCache,
UseDefaultCredentials = true
};
HttpClient httpClient = new HttpClient(handler);
The problem is, the call is always authenticated as WebUser (who runs the service), NOT AppUser (who sent the request).
I thought the DefaultNetworkCredentials were always those of the authenticated user.
How can I send (from within a WebRequest) another WebRequest to another WebService that is authenticated with the same user who submitted the original request?
Is there a way to pass the authentication from one WebService to another?
Regarding Kerberos and AD:
WebServiceA and WebServiceB are running on different servers in the same domain. They are not hosted in IIS, but in a Windows Service.
I tried to set the SPN as described here:
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth?view=aspnetcore-3.0&tabs=visual-studio
setspn -S HTTP/ServerA.domain domain\WebUser
Also tried:
setspn -a WebServiceA/WebUser domain\WebUser
But without success.
After three days of web research and trying around, my head is spinning ...
How do I get the UserCredentials into the second request?
Or is it because of (a possibly wrong?) AD configuration (SPN) that the WebService always silently takes the credentials of the user who started the service?
I am grateful for every hint!
Steffi
An answer accepted by the OP in the comment involves impersonation. According to this blog entry it's possible in .Net Core using the WindowsIdentity.RunImpersonated:
// The user to be impersonated
var userToImpersonate = (WindowsIdentity)HttpContext.User.Identity;
// Impersonating the current Windows user [HttpContext.User.Identity]...
var result = await WindowsIdentity.RunImpersonated(
userToImpersonate.AccessToken, async () =>
{
// This time WindowsIdentity.GetCurrent() will retrieve the impersonated
// user with its claims...
var impersonatedUser = WindowsIdentity.GetCurrent().Name;
// Your business logic code here...
});
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.
I am trying to add app roles to my app registration in Azure Active Directory programmatically, I am using the following Microsoft article as a reference: https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/api/application_update
Here is my code:
string bearer = "Bearer <token>";
string appId = "<guid>";
string appEndPoint = "https://graph.microsoft.com/beta/applications/{0}";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(string.Format(appEndPoint, appId));
request.Headers.Add("Authorization", bearer);
request.Method = "PATCH";
request.ContentType = "application/json";
string jsonBody = "{\"appRoles\":[{\"allowedMemberTypes\":[\"User\"],\"description\":\"This is a test role\",\"displayName\":\"Test Role\",\"id\":\"fb3d0a97-b19e-4132-bb62-4a0213b37178\",\"isEnabled\":true,\"origin\":\"Application\",\"value\":\"Test\"}]}";
request.ContentLength = Encoding.ASCII.GetBytes(jsonBody).Length;
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
{
streamWriter.Write(jsonBody);
streamWriter.Flush();
streamWriter.Close();
}
var responce = request.GetResponse(); // throws 403 Forbidden
var responseStr = new StreamReader(responce.GetResponseStream()).ReadToEnd();
This is how I am acquiring the bearer token:
string domain = "my.domain.com";
string appId = "<guid>";
string clientSecret = "<secret>";
AuthenticationContext authContext = new AuthenticationContext(string.Format("https://login.windows.net/{0}/oauth2/token", domain));
ClientCredential creds = new ClientCredential(appId, clientSecret);
AuthenticationResult result = await authContext.AcquireTokenAsync("https://graph.microsoft.com/", creds);
string bearer = result.AccessToken;
I have granted my app registration all the required permissions specified in the Microsoft article, but I keep getting a 403 response.
I have also tried granting my app registration all permissions available and still get 403, does anybody know what I am doing wrong here?
403 error means that the bearer token has insufficient privileges to complete the operation.
If we get the bearer token with Delegate permssion, we need (Directory.AccessAsUser.All), we could check it with https://jwt.io/
I also test your code on my side, it works correctly.
Note: Based on my test, if bearer token with Delegate permssion Directory.ReadWrite.All, then it has insufficient privileges
Update:
Based on my test, if I use the application permission(with AD v1 or v2), I also get the same result with you. You could give your feedback to Azure team.
APIs under the /beta version in Microsoft Graph are in preview and are subject to change. Use of these APIs in production applications is not supported.
"Directory.ReadWrite.All" is not required and is overkill. Some the service principle api's have not be migrated over to the new graph api. Try granting the below permissions, the one you are probably missing is the Azure Active Directory Graph permission
Azure Active Directory Graph - Note this takes a few minutes to apply...
Application.ReadWrite.All
Microsoft Graph
Application.ReadWrite.All
My web service is currently doing basic username/password authentication in order to subscribe the exchange user for receiving the events (like new mail event etc) like below:
var service = new ExchangeService(exchangeVersion)
{
KeepAlive = true,
Url = new Uri("some autodiscovery url"),
Credentials = new NetworkCredential(username, password)
};
var subscription = service.SubscribeToPushNotifications(
new[] { inboxFolderFoldeID },
new Uri("some post back url"),
15,
null,
EventType.NewMail,
EventType.Created,
EventType.Deleted,
EventType.Modified,
EventType.Moved,
EventType.Copied);
Now, I am supposed to replace the authentication mechanism to use OAuth protocol. I saw some examples but all of them seem to be talking about authenticating the client (https://msdn.microsoft.com/en-us/library/office/dn903761%28v=exchg.150%29.aspx?f=255&MSPPError=-2147217396) but nowhere I was able to find an example of how to authenticate an exchange user with OAuth protocol. Any code sample will help a lot. Thanks.
It's not clear what you mean with 'web service' and how you currently get the username and password. If that is some kind of website where the user needs to login or pass credentials, then you'll have to start an OAuth2 grant from the browser as in redirecting the clients browser to the authorize endpoint to start implicit grant or code grant. The user will be presented a login screen on the OAuth2 server (and not in your application), once the user logs in a code or access token (depending on the grant) will be returned to your application which you can use in the ExchangeService constructor.
If that 'web' service is some service that runs on the users computer you can use one of the methods described below.
Get AccessToken using AuthenticationContext
The example seems to be based on an older version of the AuthenticationContext class.
The other version seems to be newer, also the AcquireToken is now renamed to AcquireTokenAsync / AcquireTokenSilentAsync.
No matter which version you're using, you will not be able to pass username and password like you're doing in your current code. However, you can let the AcquireToken[Async] method prompt for credentials to the user. Which, let's be honest, is more secure then letting your application deal with those user secrets directly. Before you know, you'll be storing plain text passwords in a database (hope you aren't already).
In both versions, those methods have a lot of overloads all with different parameters and slightly different functionality. For your use-case I think these are interesting:
New: AcquireTokenAsync(string, string, Uri, IPlatformParameters) where IPlatformParameters could be new PlatformParameters(PromptBehavior.Auto)
Old: AcquireToken(string, string, Uri, PromptBehavior where prompt behavior could be PromptBehavior.Auto
Prompt behavior auto, in both vesions, means: the user will be asked for credentials when they're not already cached. Both AuthenticationContext constructors allow you to pass a token-cache which is something you can implement yourself f.e. to cache tokens in memory, file or database (see this article for an example file cache implementation).
Get AccessToken manually
If you really want to pass in the user credentials from code without prompting the user, there is always a way around. In this case you'll have to implement the Resource Owner Password Credentials grant as outlined in OAuth2 specificatioin / RFC6749.
Coincidence or not, I have an open-source library called oauth2-client-handler that implements this for use with HttpClient, but anyway, if you want to go this route you can dig into that code, especially starting from this method.
Use Access Token
Once you have an access token, you can proceed with the samples on this MSDN page, f.e.:
var service = new ExchangeService(exchangeVersion)
{
KeepAlive = true,
Url = new Uri("some autodiscovery url"),
Credentials = new OAuthCredentials(authenticationResult.AccessToken))
};
In case someone is still struggling to get it to work. We need to upload a certificate manifest on azure portal for the application and then use the same certificate to authenticate the client for getting the access token. For more details please see: https://blogs.msdn.microsoft.com/exchangedev/2015/01/21/building-daemon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow/
Using the example code in this Microsoft Document as the starting point and these libraries:
Microsoft Identity Client 4.27
EWS Managed API v2.2
I am able to successfully authenticate and connect with Exchange on Office 365.
public void Connect_OAuth()
{
var cca = ConfidentialClientApplicationBuilder
.Create ( ConfigurationManager.AppSettings[ "appId" ] )
.WithClientSecret( ConfigurationManager.AppSettings[ "clientSecret" ] )
.WithTenantId ( ConfigurationManager.AppSettings[ "tenantId" ] )
.Build();
var ewsScopes = new string[] { "https://outlook.office365.com/.default" };
AuthenticationResult authResult = null;
try
{
authResult = cca.AcquireTokenForClient( ewsScopes ).ExecuteAsync().Result;
}
catch( Exception ex )
{
Console.WriteLine( "Error: " + ex );
}
try
{
var ewsClient = new ExchangeService();
ewsClient.Url = new Uri( "https://outlook.office365.com/EWS/Exchange.asmx" );
ewsClient.Credentials = new OAuthCredentials( authResult.AccessToken );
ewsClient.ImpersonatedUserId = new ImpersonatedUserId( ConnectingIdType.SmtpAddress, "ccc#pppsystems.co.uk" );
ewsClient.HttpHeaders.Add( "X-AnchorMailbox", "ccc#pppsystems.co.uk" );
var folders = ewsClient.FindFolders( WellKnownFolderName.MsgFolderRoot, new FolderView( 10 ) );
foreach( var folder in folders )
{
Console.WriteLine( "" + folder.DisplayName );
}
}
catch( Exception ex )
{
Console.WriteLine( "Error: " + ex );
}
}
The Microsoft example code did not work - the async call to AcquireTokenForClient never returned.
By calling AcquireTokenForClient in a separate try catch block catching a general Exception, removing the await and using .Result, this now works - nothing else was changed.
I realise that this is not best practice but, both with and without the debugger, the async call in the original code never returned.
In the Azure set-up:
A client secret text string was used - a x509 certificate was not necessary
The configuration was 'app-only authentication'
Hope this helps someone avoid hours of frustration.
I am trying to download a user's mailbox using the Email Audit API. I am getting a 403 Forbidden response to this code (the error occurs on the last line, the call to the UploadPublicKey method):
var certificate = new X509Certificate2(System.Web.HttpRuntime.AppDomainAppPath + "key.p12", "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { "https://apps-apis.google.com/a/feeds/compliance/audit/" }
}.FromCertificate(certificate));
credential.RequestAccessTokenAsync(System.Threading.CancellationToken.None).Wait();
DebugLabel.Text = credential.Token.AccessToken;
var requestFactory = new GDataRequestFactory("My App User Agent");
requestFactory.CustomHeaders.Add(string.Format("Authorization: Bearer {0}", credential.Token.AccessToken));
AuditService aserv = new AuditService(strOurDomain, "GoogleMailAudit");
aserv.RequestFactory = requestFactory;
aserv.UploadPublicKey(strPublicKey);
I have created the service account in the Developers Console and granted the Client ID access to https://apps-apis.google.com/a/feeds/compliance/audit/ in the Admin console.
Seems to me like the account should have all the permissions it needs, yet it doesn't. Any idea what I am missing?
OK, so I gave up on trying to make it work with a service account even though that is what Google's documentation would lead you to believe is the correct way to do it. After emailing Google support, I learned I could just use OAuth2 for the super user account that created the application on the developer's console.
So then I worked on getting an access token for offline access (a refresh token) by following the process outlined here:
Youtube API single-user scenario with OAuth (uploading videos)
and then taking that refresh token and using it with this code:
public static GOAuth2RequestFactory RefreshAuthenticate(){
OAuth2Parameters parameters = new OAuth2Parameters(){
RefreshToken = "<YourRefreshToken>",
AccessToken = "<AnyOfYourPreviousAccessTokens>",
ClientId = "<YourClientID>",
ClientSecret = "<YourClientSecret>",
Scope = "https://apps-apis.google.com/a/feeds/compliance/audit/",
AccessType = "offline",
TokenType = "refresh"
};
OAuthUtil.RefreshAccessToken(parameters);
return new GOAuth2RequestFactory(null, "<YourApplicationName>", parameters);
}
which is code from here https://stackoverflow.com/a/23528629/5215904 (Except I changed the second to last line... for whatever reason the code shared did not work until I made that change).
So there I was finally able to get myself an access token that would allow me access to the Email Audit API. From there everything was a breeze once I stopped trying to mess around with a service account.