I have a chatbot that works on localhost, and it's working great. I then added a new Bot Channels Registration on Azure for testing, and that works fine too. I did it by taking its Microsoft App ID and password and putting it into my appsettings.json file.
However, I need to add another Bot Channels Registration. When I test it on that registration, my bot returns a 401 unauthorized error. It's because that has a new App ID and password. But I already put the App ID and password from my first registration channel. I need both of them to work.
How can I allow my chatbot to accept multiple App IDs and passwords? Or how do I get rid of that level of security completely (ie. Allow ALL App IDs and passwords)?
The answer, as #Mick suggested, is to create a bot adapter for each channel. You can do something like this if you want it really dynamic:
BotController.cs
[HttpPost, HttpGet]
public async Task PostAsync()
{
var credentialProvider = new SimpleCredentialProvider(YourAppId, YourAppPassword); // for each adapter
Adapter = new BotFrameworkHttpAdapter(credentialProvider); // for each adapter
await Adapter.ProcessAsync(Request, Response, Bot);
}
With a custom ICredentialProvider the appid and password can be retrieved from anywhere:
public class MultiCredentialProvider : ICredentialProvider
{
public Dictionary<string, string> Credentials = new Dictionary<string, string>
{
{ "YOUR_MSAPP_ID_1", "YOUR_MSAPP_PASSWORD_1" },
{ "YOUR_MSAPP_ID_2", "YOUR_MSAPP_PASSWORD_2" }
};
public Task<bool> IsValidAppIdAsync(string appId)
{
return Task.FromResult(this.Credentials.ContainsKey(appId));
}
public Task<string> GetAppPasswordAsync(string appId)
{
return Task.FromResult(this.Credentials.ContainsKey(appId) ? this.Credentials[appId] : null);
}
public Task<bool> IsAuthenticationDisabledAsync()
{
return Task.FromResult(!this.Credentials.Any());
}
}
Then, in Startup.cs:
services.AddSingleton<ICredentialProvider, MultiCredentialProvider>();
Related
TLDR version
Is there an API to do this - https://support.google.com/webmasters/answer/7687615?hl=en, I want to be able to map service account to user using some API, (I am able to do it manually, but the list is long)
Long version
From what I understand there are 2 types of users
Type 1 - Normal user (human) logging in and using google search console
Type 2 - Google service accounts, used by application to pull data
Now I want to add several hundreds of site in Google Search Console, I found C# clients/API to do that.
I am able to add/list sites using normal user account using API, and then verify by using UI to see them getting added.
I am able (no error returned) to add/list sites using service accounts using API, but then unable to
see service account user being added in the user list of the site. But I still see the site when I call the list api
when pulling data for this site, I get errors
Google.Apis.Requests.RequestError
User does not have sufficient permission for site 'https://www.example.com/th-th/city/'. See also: https://support.google.com/webmasters/answer/2451999. [403]
Errors [Message[User does not have sufficient permission for site 'https://www.example.com/th-th/city/'. See also: https://support.google.com/webmasters/answer/2451999.] Location[ - ] Reason[forbidden] Domain[global]
It points me to this link - https://support.google.com/webmasters/answer/7687615?visit_id=1621866886080-4412438468466383489&rd=2 where I can use the UI and manually add my service account and then everything works fine.
But I want to do the same thing via API, because I will be having hundreds of sites to add to.
Please advice on how to go about this one?
Seems like this user also had similar problem, but no solution - How to connect Google service account with Google Search Console
CODE
This is the code I use to create site using normal user and client id/secret, here if I create a site I am able to see it on UI but the API (https://developers.google.com/webmaster-tools/search-console-api-original/v3/sites/add) does not have option to use service account.
public class WebmastersServiceWrapper
{
private string user = "realemail#example.com";
private readonly ClientSecrets _clientSecrets = new ClientSecrets
{
ClientId = "example.apps.googleusercontent.com",
ClientSecret = "example"
};
private readonly string[] _scopes = {
WebmastersService.Scope.WebmastersReadonly,
WebmastersService.Scope.Webmasters
};
public async Task<WebmastersService> GetWebmastersService()
{
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(_clientSecrets, _scopes, user, CancellationToken.None);
var service = new WebmastersService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "WebMasters API Sample",
});
return service;
}
}
public class WebMasterSiteService
{
private readonly WebmastersServiceWrapper _connection;
public WebMasterSiteService()
{
_connection = new WebmastersServiceWrapper();
}
public async Task<IEnumerable<string>> GetSites()
{
var service = await _connection.GetWebmastersService();
var sitesResponse = await service.Sites.List().ExecuteAsync();
return SiteMapper.MapSites(sitesResponse);
}
public async Task DeleteSite(string site)
{
var service = await _connection.GetWebmastersService();
var response = await service.Sites.Delete(site).ExecuteAsync();
return;
}
public async Task AddSite(string site)
{
var service = await _connection.GetWebmastersService();
var response = await service.Sites.Add(site).ExecuteAsync();
return;
}
}
Here is the piece of code where I create sites using service worker, it gets created somewhere (as when I call list I get it back) but when I query that site using other APIs it fails with this error
Google.Apis.Requests.RequestError
User does not have sufficient permission for site 'https://www.example.com/th-th/city/'. See also: https://support.google.com/webmasters/answer/2451999. [403]
Errors [
Message[User does not have sufficient permission for site 'https://www.example.com/th-th/city/'. See also: https://support.google.com/webmasters/answer/2451999.] Location[ - ] Reason[forbidden] Domain[global]
]
public class SearchConsoleServiceWrapper
{
private readonly string[] _scopes = {
SearchConsoleService.Scope.WebmastersReadonly,
SearchConsoleService.Scope.Webmasters
};
public SearchConsoleService GetWebmastersService()
{
using var stream = new FileStream("key-downloaded-from-console-cloud-google.json", FileMode.Open, FileAccess.Read);
var credential = GoogleCredential.FromStream(stream)
.CreateScoped(_scopes)
.UnderlyingCredential as ServiceAccountCredential;
return new SearchConsoleService(new BaseClientService.Initializer
{
HttpClientInitializer = credential
});
}
}
public class SiteService
{
private readonly SearchConsoleServiceWrapper _connection;
public SiteService()
{
_connection = new SearchConsoleServiceWrapper();
}
public async Task<IEnumerable<string>> GetSites()
{
var service = _connection.GetWebmastersService();
var sitesResponse = await service.Sites.List().ExecuteAsync();
return SiteMapper.MapSites(sitesResponse);
}
public async Task DeleteSite(string site)
{
var service = _connection.GetWebmastersService();
var response = await service.Sites.Delete(site).ExecuteAsync();
return;
}
public async Task AddSite(string site)
{
var service = _connection.GetWebmastersService();
var response = await service.Sites.Add(site).ExecuteAsync();
return;
}
}
Final thoughts
Maybe I am missing something simple, also I haven't found a way to establish a relationship between my google search console account and my service account. But when I use my service account and add it as a user manually on a site, everything works and I am able to query properly.
I have recently setup an account in Google APIs and Services. In this account I enabled Google Drive integration. In the Drive UI Integration section, I have setup the Application Name as per this screenshot: https://www.screencast.com/t/5VphcOkfAXyN
In the Credentials section which I setup for this application, I also entered my application name: https://www.screencast.com/t/3XudlhbE
When I connect to my account via OpenAuth, I get a confirmation email to the connected account, from Google, which tells me that 'Quickstart connected to your Google Account'. I am expecting that the email would tell me 'My App Name connected to your Google Account': https://www.screencast.com/t/fmSXswXS
I have searched through my Google Account settings but I can't find anywhere where it says 'Quickstart'. What do I need to do in order to ensure that my actual app name is the one that appears on the confirmation email, and in the 'Apps with access to your account' permissions page (https://www.screencast.com/t/hcHSkDr5)?
Here is my FlowMetadata as requested in the comments:
public class GoogleDriveAppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = AppConfig.CloudStorage.Google.Drive.ClientId,
ClientSecret = AppConfig.CloudStorage.Google.Drive.ClientSecret,
},
Scopes = new[] { DriveService.Scope.Drive },
DataStore = new GoogleDriveDataStore(),
});
public override string AuthCallback
{
get { return #"/GoogleDriveAuthCallback/IndexAsync"; }
}
public override string GetUserId(Controller controller)
{
if (controller.HttpContext.Session[SessionConstants.CloudStorageAccount] == null)
throw new Exception("CloudStorageAccount was not populated!");
CloudStorageDto cs = (CloudStorageDto)controller.HttpContext.Session[SessionConstants.CloudStorageAccount];
return $"{controller.User.GetSessionToken()}_{cs.Id}";
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
I found the setting in the OAuth Consent Screen tab - this is the one that I needed: https://screencast.com/t/cI4lZKmNWeKp
I'm trying to achieve the following:
Have an unauthenticated user navigate to a web page, where a SignalR (core) client will connect to a hub (say Notifications hub).
Have the user perform an action and, when the operation is completed on the server, use SignalR to notify him of the completion.
The problem: when a user is logged, I find his SignalR connectionId by a connectionId-username map that is saved in memory. Then I do:
hub.SendConnectionAsync(connectionId, "Message", data);
If the user is not authenticated, I came up with using SessionId, and the map I save in memory is something that gives me a ConnectionId given a SessionId. The code snippet I use on the HubLifetimeManager is something like:
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await _wrappedHubLifetimeManager.OnConnectedAsync(connection);
_connections.Add(connection);
string userId;
if (connection.User.Identity.IsAuthenticated)
{
userId = connection.User.Identity.Name;
}
else
{
var httpContext = connection.GetHttpContext();
if (httpContext == null)
{
throw new Exception("HttpContext can't be null in a SignalR Hub!!");
}
var sessionId = httpContext.Session.Id;
userId = $"{Constants.AnonymousUserIdentifierPrefix}{sessionId}";
}
await _userTracker.AddUser(connection, new UserDetails(connection.ConnectionId, userId));
}
Problem: if my page is opened in an iframe, httpContext.Session.Id is the empty string, it looks like the cookies of my page opened in the iframe (among which is the Session cookie), are not added to the http requests performed by the javascript code executed inside the iframe...
More generally, how do you identify a user if he's not authenticated? Is there anything in the HttpRequest that you can use as a unique id, like machine name or ip?
If you want to identify an anonymous user you could use a custom http header generated on frontend. It can be accessed with IHttpContextAccessor in combination with custom IUserIdProvider:
public class CustomUserIdProvider : IUserIdProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomUserIdProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string GetUserId(HubConnectionContext connection)
{
if (connection.User.Identity.IsAuthenticated)
{
return connection.User.Identity.Name;
}
var username = _httpContextAccessor.HttpContext?.Request.Headers["username"];
if (username.HasValue && !StringValues.IsNullOrEmpty(username.Value))
{
return username.Value;
}
return Guid.NewGuid().ToString();
}
}
Remember that in .NET Core you need to explicitly add IHttpContextAccessor to the DI container:
services.AddHttpContextAccessor();
services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
services.AddSignalR();
Then you can use the generated identifier in hub method like this:
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await _wrappedHubLifetimeManager.OnConnectedAsync(connection);
_connections.Add(connection);
string userId = connection.UserIdentifier;
await _userTracker.AddUser(connection, new UserDetails(connection.ConnectionId, userId));
}
Source: https://dejanstojanovic.net/aspnet/2020/march/custom-signalr-hub-authorization-in-aspnet-core/
I have a web api in my organization built with aspnet core. We want to publish that api to be consumed by an android app, a mvc5 app and an aspnet core mvc6 app. How can I configure the web api in azure so that the apps that consume it don't ask to login. The web apps, are already protected with azure, but when I protect the web api with azure I get a 401 when I make a request to it. I don't know how to configure the app in azure or the code I must configure in the api. I've read a lot but I don't find a way to acomplish this. All I want is to login in my web app, and the web app starts to ask data to the web api through ajax. I should send in the ajax request some sort of bareer token, but i don`t know what config i must do in azure and in the apps. I hope you can help me.
After you protected the web API with Azure AD, we need to send to access token with request for the web API for authorization. And we can get the access token when the users call the web API from web app. Here is the code to acquire the token in the web app for your reference:
public async Task<IActionResult> Index()
{
AuthenticationResult result = null;
List<TodoItem> itemList = new List<TodoItem>();
try
{
string userObjectID = (User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
AuthenticationContext authContext = new AuthenticationContext(Startup.Authority, new NaiveSessionCache(userObjectID, HttpContext.Session));
ClientCredential credential = new ClientCredential(Startup.ClientId, Startup.ClientSecret);
result = await authContext.AcquireTokenSilentAsync(Startup.TodoListResourceId, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
//
// Retrieve the user's To Do List.
//
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, TodoListBaseAddress + "/api/todolist");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
//
// Return the To Do List in the view.
//
if (response.IsSuccessStatusCode)
{
List<Dictionary<String, String>> responseElements = new List<Dictionary<String, String>>();
JsonSerializerSettings settings = new JsonSerializerSettings();
String responseString = await response.Content.ReadAsStringAsync();
responseElements = JsonConvert.DeserializeObject<List<Dictionary<String, String>>>(responseString, settings);
foreach (Dictionary<String, String> responseElement in responseElements)
{
TodoItem newItem = new TodoItem();
newItem.Title = responseElement["title"];
newItem.Owner = responseElement["owner"];
itemList.Add(newItem);
}
return View(itemList);
}
else
{
//
// If the call failed with access denied, then drop the current access token from the cache,
// and show the user an error indicating they might need to sign-in again.
//
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
var todoTokens = authContext.TokenCache.ReadItems().Where(a => a.Resource == Startup.TodoListResourceId);
foreach (TokenCacheItem tci in todoTokens)
authContext.TokenCache.DeleteItem(tci);
ViewBag.ErrorMessage = "UnexpectedError";
TodoItem newItem = new TodoItem();
newItem.Title = "(No items in list)";
itemList.Add(newItem);
return View(itemList);
}
}
}
catch (Exception ee)
{
if (HttpContext.Request.Query["reauth"] == "True")
{
//
// Send an OpenID Connect sign-in request to get a new set of tokens.
// If the user still has a valid session with Azure AD, they will not be prompted for their credentials.
// The OpenID Connect middleware will return to this controller after the sign-in response has been handled.
//
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme);
}
//
// The user needs to re-authorize. Show them a message to that effect.
//
TodoItem newItem = new TodoItem();
newItem.Title = "(Sign-in required to view to do list.)";
itemList.Add(newItem);
ViewBag.ErrorMessage = "AuthorizationRequired";
return View(itemList);
}
//
// If the call failed for any other reason, show the user an error.
//
return View("Error");
}
And below is the code sample which use JwtBearerAppBuilderExtensions to add OpenIdConnect Bearer authentication capabilities to an HTTP application pipeline for the web API to verify the token:
public class Startup
{
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// Add the console logger.
loggerFactory.AddConsole(LogLevel.Debug);
// Configure the app to use Jwt Bearer Authentication
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Authority = String.Format(Configuration["AzureAd:AadInstance"], Configuration["AzureAD:Tenant"]),
Audience = Configuration["AzureAd:Audience"],
});
}
}
The full code sample you can refer here.
Note: to run this sample successfully, we need to modify the Title and Owner to lowercase title, owner in the ToDoController of web app:
foreach (Dictionary<String, String> responseElement in responseElements)
{
TodoItem newItem = new TodoItem();
newItem.Title = responseElement["title"];
newItem.Owner = responseElement["owner"];
itemList.Add(newItem);
}
You can use Azure OpenIdConnect for federated authentication. A good article from microsoft below -
Calling a web API in a web app using Azure AD and OpenID Connect
I have a fairly simple site which allow users to connect via facebook.
I am using C# facebook sdk MVC.
At first i didn't need any specific permission so there were no problems for users to connect. my code looked like this
public class FacebookController : BaseController
{
public FacebookSession FacebookSession
{
get { return (new CanvasAuthorizer().Session); }
}
public ActionResult Profile()
{
var client = new FacebookClient(this.FacebookSession.AccessToken);
dynamic me = client.Get("me");
ViewBag.Name = me.name;
ViewBag.Id = me.id;
return View();
}
}
and on my webconfig
<facebookSettings appId="XXXXXXXXXXXXXX" appSecret="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"/>
After a while I needed more specific permissions so I added CanvasAuthorize to my action - as so
[CanvasAuthorize(Permissions = "user_about_me,user_relationships,email,publish_stream")]
public ActionResult Profile()
That got me this exception:
Exception Details: System.Exception: CanvasUrl is null or empty
So I added to my Webconfig the canvasUrl which got me the same error with out the canvasPage So now my web config has all 4
<facebookSettings appId="XXXXXXXXXX" appSecret="XXXXXXXXXXXXXXXXXXXXx" canvasUrl = "http://localhost:60606/" canvasPage = "https://apps.facebook.com/XXXXXXXXXXXX/"/>
So now my user can log in via facebook, my problem is that when he does log in he is getting redirect to my Facebook app (http://apps.facebook.com/XXXXXXXXX/facebook/profile)
instead back to my site(http://localhost:60606/facebook/profile)
How can I get the Permissions that i need and redirect my user back to my site after he logs in?
Thanks
Well, My bad
CanvasAuthorize is just as it sounds, authorization for canvas.
So you cannot use it without an app canvas on facebook.
What I should have done is use the FacebookOAuthClient
var oAuthClient = new FacebookOAuthClient(FacebookApplication.Current);
oAuthClient.RedirectUri = new Uri("XXXXXXXXXXXXXX");
var loginUri = oAuthClient.GetLoginUrl(new Dictionary<string, object> { { "state", null }, { "scope", "user_about_me,user_relationships,email" } });
return Redirect(loginUri.AbsoluteUri);