I'm using the Firebase Admin SDK with ASP.net Core and because we are using workload identity federation instead of the service account key json file i'm creating my FirebaseApp instance this way:
FirebaseApp.Create(new AppOptions()
{
Credential = GoogleCredential.FromAccessToken(serviceAccountToken),
ProjectId = projectNo
})
Where "serviceAccountToken" is a Google Access Token received in a previous step.
This works like intended, but the access token i'm using expires after 1 hour.
My problem: I couldn't find a way to update the existing FirebaseApp instance with a new access token. My best approach is to use FirebaseApp.DefaultInstance.Delete() to dispose the current instance and then create a new one, but that seems a bit heavy for just updating the access token.
Is there a better approach?
Update:
My next attempt was simply to assign a new token after the token expired via FirebaseApp.DefaultInstance.Options.Credentials = GoogleCredential.FromAccessToken (newToken), but the new credentials are not taken into account.
After reading through the 'google-api-dotnet-client' source code, I noticed that GoogleCredential.FromAccessToken uses AccessTokenCredentials internally (see github). So I tried to write my own implementation of AccessTokenCredential and implement a refresh function there, unfortunately the interfaces for it are all declared internal.
In the end, there is still no good solution.
I'm trying to wrap my head around the "modern" auth methods, and dealing with OAuth and access token for calling external services, in ASP.NET Core 5 MVC.
I have an app registration in Azure, which is set up OK - these are the API permissions for that app:
My goal is to call both MS Graph (several calls), and also MS Dynamics365, to gather some information. I've managed to set up authentication with OAuth (or is it OpenID Connect? I'm never quite sure how to tell these two apart) in my MVC app, like this (in Startup.cs):
/* in appsettings.json:
"MicrosoftGraph": {
"Scopes": "user.read organization.read.all servicehealth.read.all servicemessage.read.all",
"BaseUrl": "https://graph.microsoft.com/v1.0"
},
*/
public void ConfigureServices(IServiceCollection services)
{
List<string> initialScopes = Configuration.GetValue<string>("MicrosoftGraph:Scopes")?.Split(' ').ToList();
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddInMemoryTokenCaches();
// further service setups
}
It works fine - I'm prompted to log in, provide my credentials, and in my MVC app, I can check out the claims principals with its claims after logging in - so far, everything seems fine.
Next step is calling the downstream APIs. I studied some blog posts and tutorials and came up with this solution to fetch an access token, based on the name of a scope that I need for a given call. This is my code here (in a Razor page, used to show the data fetched from MS Graph):
public async Task OnGetAsync()
{
List<string> scopes = new List<string>();
scopes.Add("Organization.Read.All");
// fetch the OAuth access token for calling the MS Graph API
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
HttpClient client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestHeaders.Add("Accept", "application/json");
string graphUrl = "https://graph.microsoft.com/v1.0/subscribedSkus";
string responseJson = await client.GetStringAsync(graphUrl);
// further processing and display of data fetched from MS Graph
}
For the MS Graph scopes, this works just fine - I get my access token, I can pass that to my HttpClient and the call to MS Graph succeeds and I get back the desired info.
The challenge starts when trying to use the same method for getting an access token to call MS Dynamics. I was assuming that I just specify the name of the API permission that is defined in the Azure AD registration - user_impersonation - like this:
public async Task OnGetAsync()
{
List<string> scopes = new List<string>();
scopes.Add("user_impersonation");
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
// further code
}
But now I am getting nothing but errors - like this one:
An unhandled exception occurred while processing the request.
MsalUiRequiredException: AADSTS65001: The user or administrator has not consented to use the application with ID '257a582c-4461-40a4-95c3-2f257d2f8693' named 'BFH_Dyn365_Monitoring'. Send an interactive authorization request for this user and resource.
which is funny, because admin consent has been granted - so I'm not quite sure what the problem is .....
I then figured maybe I needed to add user_impersonation to the list of initial scopes (as defined in the appsettings.json and used in the Startup.ConfigureServices method) - but adding this results in another funny error:
An unhandled exception occurred while processing the request.
OpenIdConnectProtocolException: Message contains error: 'invalid_client', error_description: 'AADSTS650053: The application 'BFH_Dyn365_Monitoring' asked for scope 'user_impersonation' that doesn't exist on the resource '00000003-0000-0000-c000-000000000000'. Contact the app vendor.
Strange thing is - as you saw in the very first screenshot here - that scope IS present on the app registration - so I'm not totally sure why this exception is thrown....
Can anyone shed some light, maybe from experience of calling MS Dynamics using an OAuth token? Is this just fundamentally not possible, or am I missing a step or two somewhere?
Thanks! Marc
To get a token for user_impersonation for Dynamics (instead of Microsoft Graph), you should use the full scope value: "{your CRM URL}/user_impersonation".
The full format for the values in the scopes parameter in GetAccessTokenForUserAsync is {resource}/{scope}, where {resource} is the ID or URI for the API you're trying to access, and {scope} is the delegated permission at that resource.
When you omit the {resource} portion, the Microsoft Identity platform assumes you mean Microsoft Graph. Thus, "Organization.Read.All" is interpreted as "https://graph.microsoft.com/Organization.Read.All".
When you attempt to request a token for "user_impersonation", the request fails because such a permission has not been granted for Microsoft Graph. In fact, such a permissions doesn't even exist (for Microsoft Graph), which explains the other error you see.
I am trying to renew my external providers OAuth access tokens after they expire. They are stored in my SQL database via the signin manager once the user has logged in using the external provider originally.
The access token is used by the server to make requests on the users behalf (mostly to update my database with some external providers information every now-and-then).
I am using a .NETCore 3 preview-6 Angular starter project with built-in Identity Server (I realize this a preview - but I think my issue is more related to a lack of understanding what I should be doing!). I haven't had too much trouble getting the original external login working (using .AddAuthentication().AddOAuth(...)).
But when my service that is using the provided access token gets a 401 response, I cannot figure how to get my server to renew the access token for the user silently.
I have tried using [Authorize(AuthenticationSchemes = "MyScheme")] on my Controller but this has the external provider responding with a CORS error. I then tried HttpContext.ChallengeAsync(...) in my service, but that just didn't even seem to work at all (maybe this is related to the preview version of .NETCore?).
I have even tried to re-create the scaffolded code that returns a new ChallengeResult(...), but I get the same CORS error when called from my API. (I am unsure what black-magic is going on here to make the request appear like the referrer is the external provider.)
TL;DR (code snippet)
Ideally, I am trying to make the // TODO section work:
// This code is inside a Service I have which is called by a Controller action
var user = await _userManager.GetUserAsync(_httpContextAccessor.HttpContext.User);
var accessToken = await _userManager.GetAuthenticationTokenAsync(user, "MyScheme", "access_token");
_client.SetBearerToken(accessToken);
var response = await _client.GetAsync($"{uri}?{_query.ToString()}");
if (!response.IsSuccessStatusCode) // Just assuming 401 for simplicity
{
// TODO: renew the access token and try again
}
I am expecting to get an updated access token back that I can use to re-do the request. Ideally this would be a silent renew, or a redirect if the requested external access has changed.
Hope this makes sense, please let me know if you need more information.
Thanks
I have a very simple Azure function in C# for which I've setup Azure AD Auth. I've just used the Express settings to create an App registration in the Function configuration.
public static class IsAuthenticated
{
[FunctionName("IsAuthenticated")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "options", Route = null)]
HttpRequest req,
ILogger log)
{
return new OkObjectResult("You are " + req.HttpContext.User.Identity.Name);
}
}
When I access the function in my browser everything works as expected (if not logged in I have to login and get redirected to my API). But if I try to access my function anywhere a Bearer token is needed I get an 401 Unauthorized error. Even weirder I also can't execute the function in the Azure Portal.
But the token was aquired without a problem and added to the request:
I've tried a few different things to solve this problem. First I thought maybe it's a CORS problem (since I've also had a few of those) and just set CORS to accept *, but nothing changed.
Then I've added my API login endpoints to the redirect and tried setting the implicit grant to also accept Access tokens, it's still not working.
Is there anything I've overlooked? Shouldn't the App registration express config just work with azure functions?
EDIT:
Putting the URL to my function app in the redirects as suggested by #thomas-schreiter didn't change anything (I've tried the config in the screenshot and also just putting each of those values on it's own).
EDIT 2:
I've now also tried to aquire an Bearer token the manual way with Postman, but I still run into a 401 when calling my API.
UPDATE 2020-05-12: According to ambrose-leung's answer further below you can now add a custom issuer URL which should potentially enable you to use v2 tokens. I haven't tried this myself, but maybe this will provide useful for someone in the future. (If his answer helped you please give him an upvote and maybe leave a comment 😉)
This took forever to figure out, and there is very little information about this in the offical documentations.
But it turns out the problem was/is that Azure Functions don't support Bearer tokens generated by the oauth2/v2.0/ Azure API. Since the portal uses those (if your AD supports them) you are out of luck to be able to run the function in there.
This also explains why my postman requests didn't work, because I was also using the v2 api. After switching to v1 I could access my API (Postman doesn't allow you to add a resource_id when you use the integrated auth feature, so I had to switch to handling everything manually).
After that came the realisation that you can't use MSAL either if you are writing a JS client (Angular in my case). So one alternative is ADAL, where the Angular implementation looks kind of awkward. So I decided to use angular-oauth2-oidc which took another hour of tinkering to get it to play nicely with Azure AD.
But after all that I can finally access my API.
I really don't understand why you wouldn't allow users to access Azure Function Apps with Azure AD v2 tokens, but at least this should be so much better documented. But whatever, I can finally go to sleep.
EDIT: After I opend an issue for this, they added a note that v2 isn't supported by Azure Functions, hopefully making life easier for other people.
https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-provider-aad
I managed to get it working through postman using following configuration.
Important lesson was setting in "Allowed token audiences" and "resource" name used in postman to acquire token should be same in this case. I used the same code provided here in question. in this case app registered in Azure AD is a client and resource as well. configuration and testing through postman as follows
Acquire token in postman
Calling azure function using Postman .. Authorization header with bearer token
You can now use v2.0 tokens!
Instead of choosing 'Express' when you configure AAD, you have to choose 'Advance' and add the /v2.0 part at the end of the URL.
This is the code that I use in my console app to present the user with a login prompt, then take the bearer token for use with the Azure Function.
string[] scopes = new string[] { "profile", "email", "openid" };
string ClientId = [clientId of Azure Function];
string Tenant = [tenantId];
string Instance = "https://login.microsoftonline.com/";
var _clientApp = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority($"{Instance}{Tenant}")
.WithDefaultRedirectUri()
.Build();
var accounts = _clientApp.GetAccountsAsync().Result;
var authResult = _clientApp.AcquireTokenInteractive(scopes)
.WithAccount(accounts.FirstOrDefault())
.WithPrompt(Prompt.SelectAccount)
.ExecuteAsync().Result;
var bearerTokenForAzureFunction = authResult.IdToken;
When setting up your Active Directory authentication on your Function App, set management mode to advanced and fill in the Client ID and Issuer URL as required (and the client secret if necessary).
Importantly, under the Allowed Token Audiences, enter the Application ID URI. This can be found in your registered App Registration (in your AD) under the Expose an API option.
This is what I was missing to get authentication working on my Function App. Before I added that token audience, I would always get a 401 with a valid access token.
This Azure active directory - Allow token audiences helped me get my answer but it took me a while to realise what it was referring to. Remember, it's the Application ID URI that can be found within your App Registration.
I hope it helps!
If you are banging your head against the wall like myself and the original poster, it may be that you are allowing users to sign in from "Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)."
Note that as of May 2021, v2.0 works perfectly. If you use https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token to get a token with Postman (as described above), you will get a valid token that you can use to auth your AZ Function with.
With that said, IF a user is signed in via a personal account or an account not within your AAD, the token call made by MSAL is requested with the default Microsoft tenant id, NOT your tenant id.
THIS is why I was unable to auth my function. If you are logged in with a user in your tenant's AAD, MSAL is amazing and easy to use and everything will work as described in the documentation.
In the AAD app itself, go to Settings -> Reply URLs and verify that the url of the Function App is in the list, which has the following format: https://mycoolapp.azurewebsites.net. If it isn't, then add it.
If you use slots, you have to add it for both slots.
The only thing i can think of right now is Allowed Audience.
Go to Your Active directory settings and click Advance. Under Allowed Token Audience
Add your exact function url. It might already be there with call back url but Simply replace it with only function base url without any call back as mentioned in the picture.
Make sure when you press ok , you also save your Authentication / Authorization setting to take effect and try again after 1min or so. I tested using PostMan and passing bearer token and it works !
I'm facing the exact same issue today. The issue turned out to be the resource id that I was passing when requesting the access token.
For example, initially I was requesting a token like this, using the function URL as the resource id:
AuthenticationResult authenticationResult = authenticationContext.AcquireTokenAsync("https://myfunction.azurewebsites.net", "myClientAppIdGUID", new Uri("https://login.live.com/oauth20_desktop.srf"), new PlatformParameters(PromptBehavior.SelectAccount)).Result;
While this returned an access token, I was receiving a 401 unauthorized when using the access token to call my function api.
I changed my code to pass my function apps App Id as the resource:
AuthenticationResult authenticationResult = authenticationContext.AcquireTokenAsync("myFunctionAppIdGUID", "myClientAppIdGUID", new Uri("https://login.live.com/oauth20_desktop.srf"), new PlatformParameters(PromptBehavior.SelectAccount)).Result;
Everything works fine now.
For me this was solved when I added the scope: [clientId]/.default
per this article
I am attempting to change our old rest calls for the .net client libs and I have two-ish questions/issues which are related...
The sample app on this page
https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth
uses AuthorizeAsync to get the user to approve the authorization, then you save the credentials somewhere....
At a later point - you want to do some offline stuff, for that., it has the following
UserCredential and AuthorizationCodeFlow take care of automatically
"refreshing" the token, which simply means getting a new access token.
This is done using a long-lived refresh token, which you receive along
with the access token if you use the access_type=offline parameter
during the authorization code flow.
I've marked the two statement in bold that are in question.
How do you set this parameter using the c# client lib? AuthorizeAsync does not take in a accessType flag.
Do you even need to set this (AccessType)? I noticed that after approving the oauth screen - I received both the accessToken and the RefreshToken
Once you have the refresh token - and you need to build the credentials from the saved accesstoken and refreshtoken - do you need manually refresh the access token? or will AuthorizationCodeFlow really take care of this and do I need to remember the refreshed accessToken?
using (var stream = new FileStream("client_secrets.json", FileMode.Open, FileAccess.Read))
{
credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
new[] { BooksService.Scope.Books },
"user", CancellationToken.None, new FileDataStore("Books.ListMyLibrary"));
}
The first thing that you should look at above is "user" this is where you denote different users. FileDataStore stores the credentials by default in %appData% folder on your machine each user will have their own credentials file. I have a full write up on filedatastore.
You don't need to worry about setting it to offline access or requesting a new access token when yours expires the client library will handle all of that for you.