I'm attempting to migrate our code for using the (soon to be deprecated) Google Provisioning API to the Admin SDK Directory API, via the .NET client libraries provided by Google.
In the old Provisioning API (via the .NET client library) a call to get the groups for a domain was very simple:
Google.GData.Apps.AppsService apps = new Google.GData.Apps.AppsService(AppDomain, DomainAdminEmail, AdminPassword);
Google.GData.Apps.Groups.GroupsService service = apps.Groups;
AppsExtendedFeed appsFeed = service.RetrieveAllGroups();
Not exactly rocket science, and the only credentials required were the domain, the domain's admin email, and the admin password. As long as you can supply those three parameters, you can get the groups for any domain.
I've been trying for two days to create an equivalent call using the new Admin SDK Directory API (via the new .NET client library), and it's giving me a very hard time. The only way that I've been able to get it to work at all is to create a Service Account for the project associated with the domain's admin email, including generation of a private key file, based on a useful post by mwpreston):
//Create security certificate using private key file and password.
var certificate = new X509Certificate2(pathToPrivateKeyFile, privateKeyPassword, X509KeyStorageFlags.Exportable);
//Create a service credential using the certificate, admin email and API scopes.
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
User = adminUserEmail,
Scopes = scopes
}.FromCertificate(certificate));
//Create Directory Service using the service credential and the application name.
var dirservice = new DirectoryService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = applicationName
});
var groupsListRequest = dirservice.Groups.List();
groupsListRequest.Domain = domain;
domainGroups = groupsListRequest.Execute();
However, because the Service Account is associated with a specific domain (as specified in the admin email account for the Service Account) it can only be used to request groups from that specific domain.
We have clients with thousands of different domains. It's impractical to create a new Service Account for every client's domain (as well as a private key file).
I've searched and searched for a way to call the DirectoryService in a way that uses the same parameters (domain, admin email and password) but I can't find anything. The documentation for the Admin API .NET client library is extremely sparse and is of no help.
The Provisioning API will be deprecated on April 20th 2015, so someone else there must have been faced with this issue. Can anyone help?
You should use "impersonate" (via .setServiceAccountUser("user#example.com")) while constructing credential. Look for examples here.
You can create a project on Google Developer Console and generate a refresh/access token instead. Here's the link - https://developers.google.com/accounts/docs/OAuth2WebServer
I haven't used the client library hence can't help you with the code. You can use the 'list' API which will give you all the groups for all domains of a customer (if you provide the customerId) - https://developers.google.com/admin-sdk/directory/v1/reference/groups/list
Related
TL;DR
How do I retrieve the access_token from an Identity Google external login from (say) a controller?
Full details:
I'm working on a .net6 website with Identity and Google external login.
I have added some Google API (Drive and Youtube) functionality which worked fine locally using GoogleWebAuthorizationBroker.AuthorizeAsync to get the credentials to instantiate the services for Drive and Youtube.
When deployed to the server, I got errors due to the fact that the server doesn't support GoogleWebAuthorizationBroker.AuthorizeAsync, and found out I need to use OAuth2 as described here: https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth
The problem is that I am already using Identity, so cannot follow that documentation by the letter (I cannot get an instance of IGoogleAuthProvider injected in my classes as I would if I had .AddGoogleOpenIdConnect in my services)
I have tested that initializing the Drive and Youtube services with the access token I get from the Identity Google external logins works fine:
var token = "<ACCESS_TOKEN_VALUE>"
var credential = GoogleCredential.FromAccessToken(token);
_driveService = new DriveService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = "MyWebApp"
});
The problem is that being a website, I would have to store the refresh tokens and access tokens for all users somewhere (i.e. refresh token to db, and access token to session or in the Identity.Claims)
In my .AddGoogle() call I have the option googleOptions.SaveTokens = true; enabled, but I do not know how I can retrieve this tokens from.
Is there a proper way of storing/retrieving that token?
I work on a task that should invite users and add them in my azure active directory list. Before being able to access my app, the invited user should verify through email. This is the code I use:
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(_config["AzureAd:ClientId"])
.WithTenantId(_config["AzureAd:TenantId"])
.WithClientSecret(_config["AzureAd:ClientSecret"])
.Build();
ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
var invitation = new Invitation
{
InvitedUserEmailAddress = "mail#hotmail.com",
InviteRedirectUrl = "https://url.com/",
SendInvitationMessage = true
await graphClient.Invitations
.Request()
.AddAsync(invitation);
I found this snippet somewhere on the internet and judging by the comments, it seems to work. However, when I run my app and call this functionality, I get an error that says
Code: Unauthorized Message: Insufficient privileges to perform requested operation by the application '00000003-0000-0000-c000-000000000000'. ControllerName=MSGraphInviteAPI, ActionName=CreateInvite, URL absolute path=/...
In API permissions, I have a User.Invite.All permission under Microsoft Graph. Besides this I have User.Read as well but I don't think it's relevant for this at the moment. Has some of you stumbled upon an error like this and managed to successfully solve it? If so, would you be kind to share the solution?
You are using client_credentials flow. Which means it is not a User who is performing this task, but rather service credentials. You would need to provide Application permissions, rather than what you have set - Delegated Permissions.
If you don't see Application Permissions, its because you created an Azure AD B2C Application Registration. Rather, create the App Reg with the first option Accounts in this organizational directory only (Contoso only - Single tenant) .
These are the docs you need:
AAD Client Cred flow
MS Graph API daemon app
This is correct method for AAD and AAD B2C tenant today.
I'm developing a .Net Core 3.1 API that will connect to Microsoft Graph to update photos of company employees.
However, I searched in several places and didn't find any code or method that could help me change a user's photo without me being logged in with that user's account.
Is there any way I can change the photo of a user with Microsoft Graph without me being logged in with the same?
Remembering that my user is the administrator of Azure AD and my application has "User.ReadWrite.All" released.
Firstly, what you need is this api, and as you can see it provides the application permission so that you can use client credential flow to generate access token and use it to call the api. Client credential flow makes users don't need to sign in and generate access token, then it can meet your requirement.
Here's the detail, you can generate token by this request, pls not you need to set User.ReadWrite.All application permission for you azure ad app:
and you can call the api with the token like this:
If you found your token can't work correctly, maybe you can try to add Group.ReadWrite.All application permission to your azure ad app too, as this is a known issue mentioned in the api document.
And maybe what you need is code snippet, then you can use graph sdk to call ms graph api:
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "your_tenant_name.onmicrosoft.com";
var clientId = "azure_ad_app_client_id";
var clientSecret = "client_secret_for_the_azuread_app";
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
using var stream = new System.IO.MemoryStream(Encoding.UTF8.GetBytes(#"Binary data for the image"));
await graphClient.Users["user_id_which_you_wanna_change_photo_for"].Photo.Content
.Request()
.PutAsync(stream);
Haiya all! I have the following code that sets up my Google API:
// Open the FileStream to the related file.
using FileStream stream = new("Credentials.json", FileMode.Open, FileAccess.Read);
// The file token.json stores the user's access and refresh tokens, and is created
// automatically when the authorization flow completes for the first time.
UserCredential credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.FromStream(stream).Secrets,
new[] { SheetsService.Scope.Spreadsheets, YouTubeService.Scope.YoutubeReadonly },
"admin",
CancellationToken.None,
new FileDataStore("Token", true),
new PromptCodeReceiver()
);
// Create Google Sheets API service.
builder.Services.AddSingleton(new SheetsService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = name,
}));
// Create Youtube API service.
builder.Services.AddSingleton(new YouTubeService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = name,
}));
With this code, all my services are injected properly into the service provider I'm using. However, after 7 days of being up the ASP.Net website seems to not refresh the token, even though my token file has a refresh token. This is, as I would imagine, due to the fact OAuth tokens only last 7 days.
So, how would I get Google's API to automatically refresh my token? As this is a website, it needs to be online for long amounts of time without going down due to an expired token.
If your app is still in testing refresh tokens expire after 7 days. To have them expire longer you will need to set your app into production, and possibly verify it.
Refresh token expiration
A Google Cloud Platform project with an OAuth consent screen configured for an external user type and a publishing status of "Testing" is issued a refresh token expiring in 7 days.
web app vs installed app
I'm surprised that your code is working hosted on a website.
GoogleWebAuthorizationBroker.AuthorizeAsync is intended for installed applications. Its not going to work for Asp .net hosted on a web site. The reson it wont work is that it causes the consent browser to open on the machine the code is running on. This will work in development but as soon as you try to host it on a webserver its not going to work as the server cant spawn a local web browser.
You should be following Web applications (ASP.NET Core 3)
Which will use dependency injection
public void ConfigureServices(IServiceCollection services)
{
...
// This configures Google.Apis.Auth.AspNetCore3 for use in this app.
services
.AddAuthentication(o =>
{
// This forces challenge results to be handled by Google OpenID Handler, so there's no
// need to add an AccountController that emits challenges for Login.
o.DefaultChallengeScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// This forces forbid results to be handled by Google OpenID Handler, which checks if
// extra scopes are required and does automatic incremental auth.
o.DefaultForbidScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// Default scheme that will handle everything else.
// Once a user is authenticated, the OAuth2 token info is stored in cookies.
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogleOpenIdConnect(options =>
{
options.ClientId = {YOUR_CLIENT_ID};
options.ClientSecret = {YOUR_CLIENT_SECRET};
});
}
You dont need to worry about refreshing your access token the library will handle that for you.
Is the idea to let the ASP.NET app access Youtube and Sheets...
on behalf of the end user or
on behalf of itself (no end user involved?
In case of (1), using GoogleWebAuthorizationBroker to start an OAuth authorization flow is the right way to go, but you probably shouldn't store the refresh token permanently. Either store it for the duration of the end user session only, or don't store it at all and trigger a new OAuth authorization flow every time the access token expires.
In case of (2), you should use a service account and load credentials by using GoogleCredentials.GetApplicationDefaultAsync(). If the app runs on Google Cloud, attach a service account to the underlying compute resource; if it's running elsewhere you can use Workload identity federation or service account keys.
I need to navigate to a web site and set a number of claims for that web site to use, but I cannot find any explanation on how to do it. I must be googling the wrong words.
I'm using c# and .Net framework 4.6.1
Edit 1
I was asked to explain my challenge better. I am developing 2 separate websites. Users will always go to website A where they will authenticated using Azure B2C. Once authenticated they will perform a few actions after which they will be navigated to website B. Azure B2C passes user data to website A using Claims, which I would like to forward to website B
What you need to do is to expose a new login action in your website B. This login action can receive an encrypted token which inside contains the claims you are interested in forwarding to the site B. Once that encrypted token is received, the website B can decrypt it and recreate the claims that you are interested in. You can use the built in JwtSecurityToken Handler:
https://msdn.microsoft.com/en-us/library/system.identitymodel.tokens.jwtsecuritytokenhandler(v=vs.114).aspx
For example:
public string CreateSecurityToken(string audience, IEnumerable<Claim> claims)
{
if (claims == null) return null;
var credentials = new SigningCredentials(
new InMemorySymmetricSecurityKey(Encoding.UTF8.GetBytes("SOMEKEY")),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
"http://www.w3.org/2001/04/xmlenc#sha256");
var claimsIdentity = new ClaimsIdentity(claims, "Custom Authentication", System.Security.Claims.ClaimTypes.Name, System.Security.Claims.ClaimTypes.Role);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = claimsIdentity,
TokenIssuerName = TokenIssuer,
AppliesToAddress = "http://" + audience,
Lifetime = new Lifetime(DateTime.UtcNow, DateTime.UtcNow.AddMinutes(GetTokenExpirationMinutes(audience))),
SigningCredentials = credentials
};
var jwtHandler = new JwtSecurityTokenHandler();
var token = jwtHandler.CreateToken(tokenDescriptor);
return jwtHandler.WriteToken(token);
}
this will create a token which you can send to a login method on website B
A claim from website A will not be sent to website B.
This could work this way:
A users logs on to site A (using credentials from B2C)
User saves info into B2C (using Graph API)
Redirect to Site B
User logs on to site B (this can occur without prompting the user, same B2C)
Claim are created for site B, but they can contain values set during step 2
graph API:
https://azure.microsoft.com/en-us/documentation/articles/active-directory-b2c-devquickstarts-graph-dotnet/
If the info is already in the B2C directory, the example is even more simple, create 2 sites:
https://azure.microsoft.com/nl-nl/documentation/articles/active-directory-b2c-devquickstarts-web-dotnet/
Create a custom attribute to store the information:
https://azure.microsoft.com/nl-nl/documentation/articles/active-directory-b2c-reference-custom-attr/
Create a B2C custom attribute:
Create 2(both) B2C Applications
Create a single B2C sign-in policy using the custom attribute and add it to the claims
Use the same policy in both applications, both should see the same claim