How can I access group members with a service account? - service-accounts

I am attempting to use a service account to access members of a group. I have verified that I can do this using a normal OAuth2 token on behalf of a user, with a call to https://www.googleapis.com/admin/directory/v1/groups/{group}/members and the scope https://www.googleapis.com/auth/admin.directory.group.readonly.
I’d like to do the same with a service account, and I have added the service account email address as a group member and verified that View Members permissions are set to “All members of the group, All organization members”.
When I ask for a list of members, I receive this error:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "forbidden",
"message": "Not Authorized to access this resource/api"
}
],
"code": 403,
"message": "Not Authorized to access this resource/api"
}
}
What do I need to do to authorize this service account to see the group?

Assume that you have the following
path to your serivce-account-key.josn
Domain wide delegation enabled on the service eaccount
admin email id Because of domain wide delegation, service account can use the admin email id.
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = ["https://www.googleapis.com/auth/admin.directory.user",
"https://www.googleapis.com/auth/admin.directory.group"]
credentials = service_account.Credentials.from_service_account_file(
PATH-TO-YOUR-SERVICE-ACCOUNT-FILE,
scopes=SCOPES, subject=ADMIN-EMAIL-ID)
service = build('admin', 'directory_v1', credentials=credentials)
group = "YOUR-GROUP-EMAIL-ID"
direct_members = service.members().list(groupKey=group).execute()["members"]
print(direct_members)
# Note that the above code would give only direct members.
# To get the direct members, set the `inclueDerivedMembership`
# argument to True as below.
all_members = service.members().list(
groupKey=group, inclueDerivedMembership=True).execute()["members"]
print(all_members)
The source of truth of this answer is here.

You can follow the steps outlined in the following API docs page to create the service account and perform a domain wide delegation of authority, please bear in mind you need the email address of any user who is a member of the group (userEmail in the code snippet below) so the service account can act on their behalf:
https://developers.google.com/admin-sdk/directory/v1/guides/delegation
The page includes a Java and Python examples of how to instantiate a com.google.api.services.admin.directory.Directory object using the service account and private key created on the Google Developers Console
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(jsonFactory)
.setServiceAccountId(SERVICE_ACCOUNT_EMAIL)
.setServiceAccountScopes(DirectoryScopes.ADMIN_DIRECTORY_USERS)
.setServiceAccountUser(userEmail)
.setServiceAccountPrivateKeyFromP12File(
new java.io.File(SERVICE_ACCOUNT_PKCS12_FILE_PATH))
.build();

Related

How to mimic `Get-AzureADUser` in C#

I am working on an Azure AD B2C application and the B2C policy stores the MFA secret-key in the extension_mfaTotpSecretKey property of the user. This works and when I run Get-AzureADUser -ObjectId '<object-id>' | ConvertTo-Json, then it shows:
{
"ExtensionProperty": {
"odata.metadata": "https://graph.windows.net/<tenant-id>/$metadata#directoryObjects/#Element",
"odata.type": "Microsoft.DirectoryServices.User",
"createdDateTime": "2/4/2022 2:13:22 PM",
"employeeId": null,
"onPremisesDistinguishedName": null,
"userIdentities": "[]",
"extension_7eb927869ae04818b3aa16db92645c09_mfaTotpSecretKey": "32YZJFPXXOMHT237M64IVW63645GXQLV"
},
"DeletionTimestamp": null,
...
}
During the migration process from the old directory to the new Azure B2C directory, I also want to transfer the existing TOTP key so users don't need to reregister their TOTP key. I have spent all day to get this to work, but no luck and I really don't know what's left.
I have created an app registration in the tenant with Directory.ReadWrite.All rights, but when I read the user, then the extension is empty:
var creds = new ClientSecretCredential("<tenant-id>", "<client-id>", "<client-secret>", new TokenCredentialOptions { AuthorityHost = AzureAuthorityHosts.AzurePublicCloud });
var graphClient = new GraphServiceClient(creds, new[] { "https://graph.microsoft.com/.default" });
var user = await graphClient.Users["<object-id>"].Request().Select(u => new {u.Id, u.Extensions}).Expand(u => new { u.Extensions}).GetAsync();
If I can't read the value, then I probably can't write it. I tried using OpenTypeExtension, but I am under the impression that this is a completely different property.
I can't find any documentation that tells me how I can run Get-AzureADUser using Graph API v2 in C#.
It seems that there three possible ways to extend properties in Azure AD for an object:
AzureAD Graph extension attributes
Azure AD Open extensions
Azure AD Schema extensions
Azure B2C uses AzureAD Graph extension attributes and these should be fetched directly on the user object like this:
var graphClient = new GraphServiceClient(...);
var user = await graphClient.Users["<object-id>"].Request().Select("extension_7eb927869ae04818b3aa16db92645c09_mfaTotpSecretKey").GetAsync();
var mfaTotpSecretKey = user.AdditionalData["extension_7eb927869ae04818b3aa16db92645c09_mfaTotpSecretKey"]?.ToString();
When the user is created, then these properties can be added to the AdditionalData property of the user.
Note that Azure B2C uses the persistent claim name extension_mfaTotpSecretKey, but this is translated to extension_<client-id-without-hyphens>_mfaTotpSecretKey, where <client-id> is the client-id of the B2C extensions app (with all hyphens removed).
Extension attributes are not included by default if you use the v1 endpoint of the Microsoft Graph. You must explicitly ask for them via a $select, as per #Ramon answer. When you use a $select statement, you'll get back only the specified attributes plus the id, so pay attention and specify all the fields you need. Moreover, the SDK is misleading since you'll find the extension attributes under the AdditionalData field, not in the Extensions field.
When you are going to migrate the users to a new tenant, keep in mind that the extension attribute name will change since the middle part is the b2c-extensions appId without the dashes.
i.e.
on Tenant 1: extension_xxx_myAttribute
on Tenant 2: extension_yyy_myAttribute
When you'll try to write the extension attribute on Tenant 2 via Microsoft Graph it must already exist. If you never run your custom policies on the new tenant you can create the attribute via Microsoft Graph as well with a simple POST operation:
POST https://graph.microsoft.com/v1.0/applications/<b2c-extensions-app-objectId/extensionProperties
{
"name": "attributeName",
"dataType":"string/int/etc.",
"targetObjects": ["User"]
}
You'll get the full extension attribute name in the response (i.e. extension_xxx_attributeName)
HTH, F.
https://learn.microsoft.com/en-us/graph/api/application-list-extensionproperty?view=graph-rest-1.0&tabs=csharp
GraphServiceClient graphClient = new GraphServiceClient( authProvider );
var extensionProperties = await graphClient.Applications["{application-id}"].ExtensionProperties
.Request()
.GetAsync();

How to receive user photos in MS Teams Bot which installed in different organizations?

According to the documentation https://learn.microsoft.com/en-us/graph/api/profilephoto-get?view=graph-rest-beta - in order to get the user photo we need to perform HTTP request https://graph.microsoft.com/beta/users/{userId}/photo/$value providing the access token and user id.
Using the client credentials flow I can receive the access token for a specifiс organization (tenant id), for example:
public static String getAppAccessToken(String[] scopes) {
ConfidentialClientApplication cca;
try {
cca = ConfidentialClientApplication.builder(applicationId, ClientCredentialFactory.createFromSecret(applicationSecret))
.authority("https://login.microsoftonline.com/<<tenantId>>/")
.build();
} catch (MalformedURLException e) {
return null;
}
Set<String> scopeSet = Set.of(scopes);
ClientCredentialParameters clientCredentialParam = ClientCredentialParameters.builder(
scopeSet)
.build();
CompletableFuture<IAuthenticationResult> future = cca.acquireToken(clientCredentialParam);
return future.join().accessToken();
}
But the access token received in such way allows to receive photos only for users of the organization, which has the same tenant id with the tenant id of the bot app registered in Azure Active Directory App Registration.
Is it possible to receive the photos of the users from other organizations where my bot is installed, using client credentials flow token?
In this case I receive:
{
"error": {
"code": "UnknownError",
"message": "{\"error\":{\"code\":\"NoPermissionsInAccessToken\",\"message\":\"The token contains no permissions, or permissions can not be understood.\",\"innerError\":{\"oAuthEventOperationId\":\"d90f2331-a22a-44d5-889e-c0c14ea9129e\",\"oAuthEventcV\":\"n+NecjM040KWn+2G+e5oFQ.1\",\"errorUrl\":\"https://aka.ms/autherrors#error-InvalidGrant\",\"requestId\":\"0b6bb579-94a7-47ac-8ff9-26ee893e5cb0\",\"date\":\"2021-01-28T00:57:52\"}}}",
"innerError": {
"date": "2021-01-28T00:57:52",
"request-id": "0b6bb579-94a7-47ac-8ff9-26ee893e5cb0",
"client-request-id": "0b6bb579-94a7-47ac-8ff9-26ee893e5cb0"
}
}
}
I'm certainly no expert on auth, but here are two suggestions to look in to, in case this can help:
"https://login.microsoftonline.com/<>/" -> use "organizations" instead of tenant id
check what scopes you're using (you don't list what you're trying, so it might be this)
Just try using "https://login.microsoftonline.com/common" endpoint and see if you can get this working. Please read more on that in detail here.
You can even send REST request to individual tenant's get the access token and access the information accordingly. Please be aware of having scopes while sending request.

Azure AD v2.0-specific optional claims missing from ID Token

I'm trying to add optional claims using Microsoft Identity Web - NuGet for user authentication in NET Core 3.1 WebApp. Reading the MS Docs, it seems that the only steps needed are to declare the optional claims within the App Registration Manifest file in Azure. But when testing the login process using two different apps (my own code and an MS project example) it looks like the optional claims are not being added to the ID Token when returned from Azure following a successful login i.e they're not present at all when viweing the token details in Debug.
I'm not sure how to diagnose this and where to trace the issue i.e am I missing any required steps in Azure setup?
Side Note: Just to confirm it is the jwt ID Token I want to receive the additional claims, NOT the jwt access token used for calling the graph or another Web API endpoint.
MS Docs reference: v2.0-specific optional claims set
Below is the extract from the Manifest file: (note I've even declared the "accessTokenAcceptedVersion": 2, given that optional claims I'm using are not available in ver.1, which if the above was left at default 'null' value then Azure will assume we're using legacy ver.1 - a possible gotcha)
"accessTokenAcceptedVersion": 2,
"optionalClaims": {
"idToken": [
{
"name": "given_name",
"source": "user",
"essential": false,
"additionalProperties": []
},
{
"name": "family_name",
"source": "user",
"essential": false,
"additionalProperties": []
}
],
"accessToken": [],
"saml2Token": []
},
Extract from startup class:
public void ConfigureServices(IServiceCollection services)
{
// Added to original .net core template.
// ASP.NET Core apps access the HttpContext through the IHttpContextAccessor interface and
// its default implementation HttpContextAccessor. It's only necessary to use IHttpContextAccessor
// when you need access to the HttpContext inside a service.
// Example usage - we're using this to retrieve the details of the currrently logged in user in page model actions.
services.AddHttpContextAccessor();
// DO NOT DELETE (for now...)
// This 'Microsoft.AspNetCore.Authentication.AzureAD.UI' library was originally used for Azure Ad authentication
// before we implemented the newer Microsoft.Identity.Web and Microsoft.Identity.Web.UI NuGet packages.
// Note after implememting the newer library for authetication, we had to modify the _LoginPartial.cshtml file.
//services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
// .AddAzureAD(options => Configuration.Bind("AzureAd", options));
///////////////////////////////////
// Add services required for using options.
// e.g used for calling Graph Api from WebOptions class, from config file.
services.AddOptions();
// Add service for MS Graph API Service Client.
services.AddTransient<OidcConnectEvents>();
// Sign-in users with the Microsoft identity platform
services.AddSignIn(Configuration);
// Token acquisition service based on MSAL.NET
// and chosen token cache implementation
services.AddWebAppCallsProtectedWebApi(Configuration, new string[] { Constants.ScopeUserRead })
.AddInMemoryTokenCaches();
// Add the MS Graph SDK Client as a service for Dependancy Injection.
services.AddGraphService(Configuration);
///////////////////////////////////
// The following lines code instruct the asp.net core middleware to use the data in the "roles" claim in the Authorize attribute and User.IsInrole()
// See https://learn.microsoft.com/aspnet/core/security/authorization/roles?view=aspnetcore-2.2 for more info.
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// The claim in the Jwt token where App roles are available.
options.TokenValidationParameters.RoleClaimType = "roles";
});
// Adding authorization policies that enforce authorization using Azure AD roles. Polices defined in seperate classes.
services.AddAuthorization(options =>
{
options.AddPolicy(AuthorizationPolicies.AssignmentToViewLogsRoleRequired, policy => policy.RequireRole(AppRole.ViewLogs));
});
///////////////////////////////////
services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
// Adds the service for creating the Jwt Token used for calling microservices.
// Note we are using our independant bearer token issuer service here, NOT Azure AD
services.AddScoped<JwtService>();
}
Sample Razor PageModel method:
public void OnGet()
{
var username = HttpContext.User.Identity.Name;
var forename = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "given_name")?.Value;
var surname = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "family_name")?.Value;
_logger.LogInformation("" + username + " requested the Index page");
}
UPDATE
Getting closer to a solution but not quite there yet. Couple of issues resolved:
I originally created the Tenant in Azure to use B2C AD, even though I was no longer using B2C and had switched to Azure AD. It wasn't until I deleted the tenant and created a new one before I started to see the optional claims come through to the webapp correctly. After creating the new tenant and assigning the tenant type to use Azure AD, I then found that the 'Token Configuration' menu was now available for configuring the optional claims through the UI, it seems that modifying the App manifest is still required as well, as shown above.
I had to add the 'profile' scope as type 'delegated' to the webapp API Permissions in Azure.
The final issue still unresolved is that although I can see the claims present during Debug, I cant figure out how to retrieve the claim values.
In the method below, I can see the required claims when using Debug, but can't figure out how to retrieve the values:
public void OnGet()
{
var username = HttpContext.User.Identity.Name;
var forename = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "given_name")?.Value;
var surname = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "family_name")?.Value;
_logger.LogInformation("" + username + " requested the Index page");
}
Debug Screenshots shows the given_name & family_name are present:
I've tried different code examples using the claims principal to try and get the values out, but nothing is working for me. Hoping this final riddle is fairly simple to someone who knows the required syntax, as said we now have the required optional claims present, its just not knowing how to actually get the values out.
Big thanks to 'Dhivya G - MSFT Identity' for their assistance (see comments below my original question) method below now allows me to access the required claim values from the Token ID returned from Azure following successful login.
public void OnGet()
{
var username = HttpContext.User.Identity.Name;
var forename = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
var surname = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
_logger.LogInformation("" + username + " requested the Index page");
}

Can't add 'otherMails' with Graph-Tester

I am trying to add another email address to a User.
PATCH https://graph.microsoft.com/v1.0/users/user#domain.de
Body:
{
"givenName":"Meier",
"surname":"Meeier",
"otherMails":["emaissssl#domain.de"]
}
Response: Success - Statuscode 204
The result is givenName changed to Meier, surname changed to Meeier, but the email doesn't get added to otherMails[].
A related question is, can I change the primary address of the User?
Edit: I get the same behavior, if i also include the current address:
{
"givenName": "Meier",
"surname": "Meeier",
"otherMails": ["user#domain.de", "emaissssl#domain.de"]
}
You need spesific permissions in AAD to update otherMails. From the documentation:
Updating another user's businessPhones, mobilePhone, or otherMails property is only allowed on users who are non-administrators or assigned one of the following roles: Directory Readers, Guest Inviter, Message Center Reader, and Reports Reader. For more details, see Helpdesk (Password) Administrator in Azure AD available roles. This is the case for apps granted either the User.ReadWrite.All or Directory.ReadWrite.All delegated or application permissions.
The wording is a little poor but basically, if the User record you are updating is an Administrator or assigned any of the mentioned roles (Directory Readers, Guest Inviter, Message Center Reader, or Reports Reader), it will ignore the change request.
As for changing the primary email address, that isn't possible. The primary email address is automatically constructed based on the mailNickname and the default domain for the tenant (mailNickname#default.dom).

How do I use the Google API Explorer to test my own App Engine Endpoints using OAuth?

I have an Endpoints API deployed on App Engine. I have no problem using the Google API Explorer to make requests to API methods that do NOT require being logged in. The URL I'm using for that is:
https://developers.google.com/apis-explorer/?base=https://[MY_APP_ID].appspot.com/_ah/api
Where I am stuck is calling API methods that require the user to be logged in, such as this one:
#ApiMethod(name = "config.get",
clientIds = {"[MY_CLIENT_ID].apps.googleusercontent.com", "com.google.api.server.spi.Constant.API_EXPLORER_CLIENT_ID"},
audiences = {"[MY_APP_ID].appspot.com"},
scopes = {"https://www.googleapis.com/auth/userinfo.email"})
public Config getConfig(User user) throws OAuthRequestException {
log.fine("user: " + user);
if (user == null) {
throw new OAuthRequestException("You must be logged in in order to get config.");
}
if (!userService.isUserAdmin()) {
throw new OAuthRequestException("You must be an App Engine admin in order to get config.");
}
...
On the API Explorer there's a switch top right that, when clicked, allows me to specify scopes and authorise. I'm doing that with just the userinfo.email scope checked. It makes no difference. The response I get from my call is:
503 Service Unavailable
- Show headers -
{
"error": {
"errors": [
{
"domain": "global",
"reason": "backendError",
"message": "java.lang.IllegalStateException: The current user is not logged in."
}
],
"code": 503,
"message": "java.lang.IllegalStateException: The current user is not logged in."
}
}
Back when Endpoints was in Trusted Tester phase, I remember there being a manual step in the OAuth2 Playground to get an ID token instead of an access token or some such thing. If that is still required, any mention of that seems to have disappeared from the Endpoints docs now and I see now way to swap out tokens in the API Explorer either.
I see you've got "com.google.api.server.spi.Constant.API_EXPLORER_CLIENT_ID" in quotes. If that's not a typo in your transcription to Stack Overflow, that's a problem. The value is already a string, so you're just passing in the text com.google.api.server.spi.Constant.API_EXPLORER_CLIENT_ID (not the actual client ID) as the whitelisted scope. That won't work. Try this instead:
#ApiMethod(name = "config.get",
clientIds = {"[MY_CLIENT_ID].apps.googleusercontent.com", com.google.api.server.spi.Constant.API_EXPLORER_CLIENT_ID},
audiences = {"[MY_APP_ID].appspot.com"},
scopes = {"https://www.googleapis.com/auth/userinfo.email"})
Edit: isUserAdmin is unsupported within Endpoints, and is likely a secondary cause of error. I'd suggest filing a feature request for supporting this method on the provided User object (we likely won't provide support for the user service itself, so it's separate from OAuth login.)
I don't know when this was introduced, but if you use OAuth2, instead of UserService.isUserAdmin() you can use OAuthServiceFactory.getOAuthService().isUserAdmin(EMAIL_SCOPE) where EMAIL_SCOPE is "https://www.googleapis.com/auth/userinfo.email".
This makes it easy to use the old OpenId or OAUth2:
boolean isAdmin = false;
try {
isAdmin = userService.isUserAdmin());
} catch (IllegalStateException e1) {
try {
isAdmin = OAuthServiceFactory.getOAuthService().isUserAdmin(EMAIL_SCOPE);
} catch (Exception e2) {}
}
The original question was asked several years ago, but maybe this will help others.

Resources