I understand that Microsoft emphasizes on a proper token validation.
The following code example (link includes the exact line of code) does not include token validation:
https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/master/4-WebApp-your-API/4-2-B2C/Client/Startup.cs#L44
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, Constants.AzureAdB2C)
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { Configuration["TodoList:TodoListScope"] })
.AddInMemoryTokenCaches();
How can I improve above line of code so that it can validate tenant ID claim?
• To validate the token received from Azure AD B2C in Asp.Net, you will have to include ‘TokenValidationParameters’ value and define the validation of token claims received accordingly in the ‘Startup.cs’ file of the Web API. Please find the below sample code to be included in the ‘Startup.cs’ file for token validation which protects the Web API with Microsoft Identity platform: -
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(options =>
{
Configuration.Bind("AzureAdB2C", options);
options.TokenValidationParameters.ValidIssuers = new[] { /* list of valid issuers */ };
options.TokenValidationParameters.ValidAudiences = new[] { /* list of valid audiences */};
},
options => { Configuration.Bind("AzureAdB2C", options); });
Once the above has been done, add the method app.UseAuthentication() before app.UseMvc() in the Configure method as below: -
‘ app.UseAuthentication();
app.UseMvc(); ‘
Thus, you can add token validation parameters in your Asp.Net Web API for verifying tenant ID claims. For more detailed information regarding this, please refer to the documentation links below: -
https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/4-WebApp-your-API/4-2-B2C
https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-protected-web-api-app-configuration#token-validation
Related
I have a multi-tenant Web API of tenant A. It has permissions exposed and accepted by a B2C Web API of tenant B. (The API App Services live in the same tenant, but their AD instances are separate due to the one being a B2C tenant).
I have the following code in my B2C Web API authenticating with tenant B to access the multi-tenant Web API of tenant A.
I'm using Microsoft.Identity.Web (v1.25.5) and .NET Core (6), and so I don't have to handle making unnecessary calls to get an access token, I'm using the IDownstreamWebApi helper classes (though I have tried without according to the documentation, but land up with the same error.)
My code:
appsettings.json
program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(options =>
{
builder.Configuration.Bind("AzureAdB2C", options);
},
options => {
builder.Configuration.Bind("AzureAdB2C", options);
})
.EnableTokenAcquisitionToCallDownstreamApi(options =>
{
builder.Configuration.Bind("AzureAdB2C", options);
})
.AddDownstreamWebApi("TenantAApi", options =>
{
builder.Configuration.Bind("TenantAApi", options);
})
.AddInMemoryTokenCaches();
Calling code:
var response = await _downstreamWebApi.CallWebApiForAppAsync(
"TenantAApi",
options =>
{
options.HttpMethod = httpMethod;
options.RelativePath = url;
}, content);
var responseContent = await response.Content.ReadAsStringAsync();
The error I receive:
MSAL.NetCore.4.48.0.0.MsalClientException:
ErrorCode: tenant_override_non_aad
Microsoft.Identity.Client.MsalClientException: WithTenantId can only be used when an AAD authority is specified at the application level.
at Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder`1.WithTenantId(String tenantId)
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(String scope, String authenticationScheme, String tenant, TokenAcquisitionOptions tokenAcquisitionOptions)
at Microsoft.Identity.Web.DownstreamWebApi.CallWebApiForAppAsync(String serviceName, String authenticationScheme, Action`1 downstreamWebApiOptionsOverride, StringContent content)
What doesn't make sense is that I'm calling this from a B2C Web API, from what I can see in the existing AbstractAcquireTokenParameterBuilder code (see line 292), B2C authorities are not AAD specific, and even so, adding an Authority or AadAuthorityAudience to my AzureAdB2C config object has no effect.
Am I missing a configuration property somewhere?
It seems that this isn't possible according to the following wiki post -
https://github.com/AzureAD/microsoft-identity-web/wiki/b2c-limitations#azure-ad-b2c-protected-web-apis-cannot-call-downstream-apis
For now I'm going to try a different approach and get an access token with a ConfidentialClientApplication object, and if that doesn't work, create a separate app registration in the other tenant and authenticate with that instead.
I have .NET Core app with .NET Core Identity.
I've setup shared cookie into Startup.cs:
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<DataContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".AspNet.SharedCookie";
});
services.AddAuthentication()
...
app.UseAuthentication();
app.UseAuthorization();
Also I have 2nd .NET Core app where I don't have authentication at all but want to use just that SharedCookie and I did the following in the Startup.cs:
services.AddAuthentication("Identity.Application")
.AddCookie("Identity.Application", options =>
{
options.Cookie.Name = ".AspNet.SharedCookie";
});
...
app.UseAuthentication();
app.UseAuthorization();
and on controller actions I set attribute [Authorize].
I logged in into 1st app and go to 2nd app and see error /Account/Login... page doesn't exist.
Yes I don't have that page but why do I see this issue? Did I forget anything to add in my code?
And one more question: what's SharedCookie string? Is it random string or it's encoded some user data? can I extract any info from that SharedCookie, for example Id of User?
So my solution was to add DataProtection step to both apps:
if (!Env.IsDevelopment())
{
services.AddDataProtection()
.PersistKeysToFileSystem("{PATH TO COMMON KEY RING FOLDER}")
.SetApplicationName("SharedCookieApp");
}
And one more question: what's SharedCookie string? Is it random string or it's encoded some user data? can I extract any info from that SharedCookie, for example Id of User?
Yes, I can extract Id, Email of user in the following way:
var id = HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
var email = HttpContext.User.FindFirstValue(ClaimTypes.Email);
in my web api application I get the acess token from http:applicationpath/connect/token with some parameters (this endpoint is from Identity I think, since we dont create it neither can see it).
But now I need to generate the token from a specific controller but cant see how to do this.
Someone knows how this can be made? Or even if it's possible?
Thanks
Some more info:
My application is an integrator (is this the word?) between an android app(app1) and other web application(app2).
1- The app1 user will send the login and password to my application .
2- Then my application will send then to the app2 who will, if everything goes well, return the app2 token .
3- Then I have to save this token in my db.
4- Then verify if the user exists in my db, and if not, save it.
5- And finally generate an token for my application and return it to the user.
Based on your comment:
But can I, instead of change de default endpoint, make another
endpoint that do the same (generate the token)?
it seems that you are rather looking for Extending discovery. This is quite easy actually.
Add a custom entry in the configuration of startup:
services.AddIdentityServer(options =>
{
options.Discovery.CustomEntries.Add("custom_token", "~/customtoken");
});
And add a controller that handles the request:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
// In case a token is required for login, like the UserInfo endpoint:
//[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ApiController]
public class CustomTokenController : ControllerBase
{
[Route("customtoken")]
public IActionResult CustomTokenEndpoint()
{
return Ok();
}
}
Update
You can 'replace' the endpoint by disabling the default authorization endpoint and adding a custom endpoint as described above.
Disable the endpoint:
services
.AddIdentityServer(options =>
{
options.Endpoints.EnableAuthorizeEndpoint = false;
})
You may want to use the Authorize path constant.
public const string Authorize = ConnectPathPrefix + "/authorize";
Add the new endpoint:
services.AddIdentityServer(options =>
{
options.Discovery.CustomEntries.Add("authorization_endpoint", $"~/{Authorize}");
});
Please note, I didn't test it, but I think this should work.
I am having difficulty using the FirebaseApp (a 3rd party API) to generate an authentication token that can be passed to a sidebar and used by the client to login and access my Firebase Database client-side.
I'm trying to use this tutorial but cannot get it working without using a database secret (which is being depreciated) in makeToken(). I'd prefer to use a service account as reflected in this tutorial. When I look at the difference between the tokens generated, the first 2 pieces separated by a '.' are identical, the last piece after the final '.' is different. The lengths are the same as well. eg:
//Example Generated by Database Secret: TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBv.ZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=.dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2U=
//Example Generated by Service Account: TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBv.ZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=.IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbml=
I can generate the OAuth access token, pass it to FirebaseApp and generate an authentication token, but when it is passed client-side and I attempt to authenticate I get an error: Login Failed! Error: INVALID_TOKEN: Failed to validate MAC.
It seems like there is a lot of misinformation and conflicting information on how this should be done.
I have a getFirebaseService() function server-side that uses Apps Script OAuth2 Library to get an access token.
function getFirebaseService() {
return OAuth2.createService('Firebase')
// Set the endpoint URL.
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the private key and issuer.
.setPrivateKey(fb_PRIVATE_KEY) //Service account private key
.setIssuer(fb_SERVICE_EMAIL) //Service account email
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scopes.
.setScope('https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/firebase.database');
}
I have a makeToken() function server-side that gets an authentication token from Firebase using the OAuth access token. I am able to use the service.getAccessToken() OAuth token server-side to access and store data. So that works, I guess my issue is creating a client auth token that's more restrictive.
function makeToken(){
var service = getFirebaseService();
if (service.hasAccess()) {
return FirebaseApp.getDatabaseByUrl(fb_URL, service.getAccessToken()) //Database Secret Works: "AAslhfi3MYACCESSTOKEN2930hf03ah4th8" but is being depreciated.
.createAuthToken(Session.getActiveUser().getEmail());
} else {
Logger.log("makeToken: " + service.getLastError());
}
}
Then client-side, from the sidebar, I try to authenticate with a custom auth token retrieved server-side from makeToken().
var userAuthToken;
google.script.run.withSuccessHandler(function (requestAuthToken) {
userAuthToken = authenticateClient(requestAuthToken)
}).makeToken();
function authenticateClient(userRequestToken) {
var ref = new Firebase(fb_URL);
ref.authWithCustomToken(userRequestToken, function (error, authData) {
if (error) {
console.log("FB Login Failed!", error); //Error below come from here.
}
else {
console.log("FB Login Succeeded!", authData);
}
});
return ref.authData.auth;
}
This results in Login Failed! Error: INVALID_TOKEN: Failed to validate MAC..
Edit: Is it possible FirebaseApp is incorrectly generating the JWT Authentication Token?
Edit2: I think the above edit is unlikely as I attempted to use the GSApp library and had the same issue. It only seems to want the depreciated database secret, not a service account OAuth.
Alright, so after a very long day I figured it out. I'm going to lay out what I ended up using for libraries and what the issue was (see the third library). The main problem was essentially that the tutorial was outdated and no a lot of people use Firebase in apps script.
OAuth2 (Server-side)
Link
I didn't have to change anything here! It was working fine and never an issue.
FirebaseApp (Server-side)
Link
This is a nice library and I stuck with it because it worked well (once I got it there). I had to make a change to my original code that came from the tutorial I mentioned. My code ended up like this and worked:
if (service.hasAccess()) {
return FirebaseApp.getDatabaseByUrl(fb_URL, service.getAccessToken()) //get OAuth Token
.createAuthToken(Session.getEffectiveUser().getEmail(), null, serviceAccount.client_email, serviceAccount.private_key);
//... Added the null, private key, and service email parameters.
Firebase (Client-side)
Link
Alright, so this is where my main issue was -- The tutorial I followed for client-side setup was old. I had to upgrade the code on my own to use the new 3.x version:
<script src="https://www.gstatic.com/firebasejs/5.8.2/firebase.js"></script>
// Initialize Firebase
var config = {
apiKey: "<Web API Key>",
authDomain: "<Project ID>.firebaseapp.com",
databaseURL: "https://<DB URL>.firebaseio.com/"
};
firebase.initializeApp(config);
With this firebase instance I was able to update my original authenticateClient() method:
function authenticateClient(userRequestToken) {
firebase.auth().signInWithCustomToken(userRequestToken).catch(function(error) {
// Handle Errors here.
console.error("authClient: ", error.code, error.message);
});
return {
uid: firebase.auth().currentUser.uid,
metadata: {
lastSignInTime: firebase.auth().currentUser.lastSignInTime
}
};
}
That's it! I now have a firebase instance with a signed in user via JWT Custom Token! I came across a few people with similar issues an I hope this helps.
I'm using HTTP.post in meteor and I need to send basic authentication with only a username to an external service. Where does this go and what would that look like?
I am only using it on the server side so I know it should look like the below code, but I'm not sure where to put the username and what to call it.
I've tried this.
var resultSet = HTTP.post("https://billy.balancedpayments.com/v1/customers", {
params: {"processor_uri": "/customers/customerURI"},
authentication: {"MYKEYHERE":""}
});
And this.
var resultSet = HTTP.post("https://billy.balancedpayments.com/v1/customers", {
params: {"authentication": "MYKEYHERE",
"processor_uri": "/customers/customerURI"}
});
And this.
var resultSet = HTTP.post("https://billy.balancedpayments.com/v1/customers", {
params: {"processor_uri": "/customers/customerURI"
},
headers: {'Authorization': 'MYKEYHERE'}
});
I get this error each time.
Error: failed [403] 403 Forbidden Access was denied to this resource.
Unauthorized: CustomerIndexView failed permission check
The plain auth : 'username:password' should do (from docs):
var resultSet = HTTP.post("https://billy.balancedpayments.com/v1/customers", {
params: {"processor_uri": "/customers/customerURI"},
auth: 'yourkey:'
});
As per the balanced payments documentation:
To authenticate with Balanced, you will need the API key secret provided from the dashboard. You have to use http basic access authentication. Your key has to be set as the username. A password is not required for simplicity.
So this means you leave the password blank, so its just your key followed by the colon :
Also you might want to consider using the balanced package for Meteor which does all the boilerplate for you.