Configuring adal.js for multiple applications/environments - azure-active-directory

When setting up a new Azure Web App for a new environment (QA) of an already existing application (production), I'm trying to create a conditional logic that will authenticate against the QA app registration instead of the production app registration. The idea is to maintain a separation between which redirect URIs are allowed to maintain independence between the environments.
It makes sense to me to not allow redirection across to a different app than the one the authorization targets, but perhaps a viable solution is to just allow redirection to the QA URI? However, this would create a cross-environment dependency I am not very fond of.
Assuming the separation of allowed redirect URIs is maintained, conditional logic in the ADAL config is required so that a user trying to login to the QA app won't authenticate against the production app, since that app registration will not allow redirection to the QA URI.
The QA Azure Web App is being set up on a different subscription but same tenant as our production web app. I already separate (localhost) development config using process.env.NODE_ENV (which is set in start.js/build.js), but since the QA server is supposed to be production-like, NODE_ENV would be set to 'production' here and as such the same method cannot be used to differentiate between QA and production.
With few other options in mind, I tried differentiating between QA and production based on window.location.href (see code sample below)
import { AuthenticationContext } from 'react-adal';
var adalConfig = {
tenant: 'tenant-id',
endpoints: {
api: 'https://graph.microsoft.com',
},
postLogoutredirectUri: window.location.origin,
cacheLocation: 'localStorage'
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
adalConfig.clientId = 'development-web-app-client-id';
} else if (window.location.href === 'QA URI') {
adalConfig.clientId = 'qa-web-app-client-id';
} else
adalConfig.clientId = 'production-web-app-client-id';
}
var authContext = new AuthenticationContext(adalConfig);
if (authContext.isCallback(window.location.hash)) {
authContext.handleWindowCallback();
var err = authContext.getLoginError();
if (err) {
// TODO: Handle errors signing in and getting tokens
}
}
export {adalConfig};
export {authContext};
export const getToken = () => {
return authContext.getCachedToken(authContext.config.clientId);
};
This caused an infinite loop of authentication attempts when trying to login to the QA web app, and for some reason also got the visitors of the production web app to authenticate against the QA app registration (authentication was actually successful once the QA app registration was updated to allow redirecting to the production URI).
It was a shot in the dark and now I am unsure how to best proceed. A shortage of relevant search results leads me to think that there is a significant difference between what I am trying to achieve and what is best practice. A bump in the right direction would be much appreciated!

Using window.location.host instead of href, the conditional worked as expected and it is possible to have different clientId depending on the host.

Related

Authorization on a Blazor Server app using Microsoft Identity (Azure AD Authentication) doesn't work

Forgive me if this is an obvious question with an easy answer but for the life of me I cannot get my app to behave the way I want.
When I normally use MS Identity in my Blazor apps I can create roles and policies which all come from a SQL database. For this B2B app I need to use Azure AD and the groups within there to authenticate and authorize access.
At the moment the whole app is secured because the default policy is being applied to all parts of the site but I would like to use [Authorize(Policy = "ViewCustomer")] for example to ensure users have the right permission to view a particular part.
I am sure that this part of my program.cs is missing policies and is part of the problem:
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
Trouble is I don't have a clue how to create these so they refer to groups (or similar) in the Azure AD tenant. My complete program.cs is:
using DevExpress.Blazor;
using DataModel.Models;
using Microsoft.EntityFrameworkCore;
using BlazorUI.Hubs;
using BlazorUI.Services;
using Xero.NetStandard.OAuth2.Config;
using BlazorUI.Services.Interfaces;
using DataModel.Xero.Interface;
using DataModel.DickerData.Interfaces;
using DataModel.DickerData;
using DataModel.Xero;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
var builder = WebApplication.CreateBuilder(args);
var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ') ?? builder.Configuration["MicrosoftGraph:Scopes"]?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
.AddInMemoryTokenCaches();
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
builder.Services.AddScoped<ISettingService, SettingService>();
builder.Services.AddScoped<IXeroService, XeroService>();
builder.Services.AddScoped<IDickerDataService, DickerDataService>();
//XERO SETTINGS
builder.Services.Configure<XeroConfiguration>(builder.Configuration.GetSection("XeroConfiguration"));
//DICKER DATA SETTINGS
builder.Services.Configure<DickerConfig>(builder.Configuration.GetSection("DickerDataConfiguration"));
//DEVEXPRESS
builder.Services.AddDevExpressBlazor(configure => configure.BootstrapVersion = BootstrapVersion.v5);
//ENTITY FRAMEWORK
builder.Services.AddDbContextFactory<ApplicationDBContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DBConnection"));
options.EnableSensitiveDataLogging();
});
var app = builder.Build();
//DEVEXPRESS
builder.WebHost.UseWebRoot("wwwroot");
builder.WebHost.UseStaticWebAssets();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
//REGISTER SIGNAL R HUBS
app.MapHub<MessageHub>(MessageHub.PATHTOHUB);
app.Run();
Thank you so much to anyone that may be able to enlighten me.
You can add app roles to your application, assign them to selected users and obtain them as claims so they can be used in tandem with your AuthorizeAttribute.
Alternatively, you can further customize or augment the authorization criteria using Claim Based Authorization in tandem with your policy.

MSAL React not showing roles, but they are in the token

I'm trying to implement App Roles in a single-tenant React + .NET Core app of ours. This app has successfully been authenticating users via MSAL, so this is just an incremental addition.
I have the app roles set up in Azure AD, and I have the Authorize attribute with role restrictions working in the .NET back-end, but for some reason I'm unable to get the roles via the react MSAL library, even though when I manually decode the token I see them in there.
I was referring to this MS sample for my code. In my index.js, I have the following:
export const msalInstance = new PublicClientApplication(msalConfig);
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0]);
}
Then, in the test page I have, I'm trying to access the roles array in this way (just as a test to print them out):
const TestComponent = () => {
const { instance } = useMsal();
useEffect(() => {
const activeAccount = instance.getActiveAccount();
setTokenRoles(activeAccount?.idTokenClaims?.roles);
// I've also tried:
// setTokenRoles(activeAccount?.idTokenClaims['roles']);
}, [instance]);
return (
<div>
ROLES: {JSON.stringify(tokenRoles)}
</div>
);
};
Unfortunately, tokenRoles is null. When I inspect entire idTokenClaims object, I see all the other claims, but no roles. However, I do see them in the token itself:
{
...
"roles": [
"Packages.Manage"
],
...
}
I'm really hoping to avoid manually decoding the token. There has to be a way to get it out of MSAL.
Jason Nutter's comments provided the answer, and in case this helps others I figured I'd give it a write-up.
Per the MS docs, I put the app roles on the back-end app registration. This is why I am able to have the Authorize(Roles = "Role") attribute work on the back-end. The reason I can see the roles in the access token is that the token is retrieved with the scope for that back-end API. But because I don't have those roles mirrored on the front-end app registration, I don't see anything in the id token.
There would be two options if you wanted to use the Azure app roles:
Mirror the app roles in the front-end app registration. In this way you'd have access to the roles in the id token. This sounds not good because I could foresee a typo or mismatch causing weird issues. I'm sure there might be a way using the Azure API to have a process that would sync the roles, but that's not worth it in my opinion.
Manually decode the access token on the front-end. The cleanest way I could think of to do this would be to create a roles context that would pull an access token, decode it, and store the roles for child components to refer to.
Another alternative would be to manage roles in the app itself. For us, the application in question is single-tenant, so there's not much need to do that. However, we do have a multitenant app we are moving to MSAL, and in that case we will already need to do things like validate that the tenant is authorized, and we will need more granular permissions than what this internal app needs, so we will likely have role system and have the front-end retrieve role and profile data from the back-end upon authentication through MSAL.
EDIT: What I ultimately did...
I did indeed keep the roles in the back-end only, then created a user context object that the front-end would retrieve. This user context includes the app roles, as well as other convenience data points like nickname, and is used by a React context and provider that I wrap my app in.

Service to service requests on App Engine with IAP

I'm using Google App Engine to host a couple of services (a NextJS SSR service and a backend API built on Express). I've setup my dispatch.yaml file to route /api/* requests to my API service and all other requests get routed to the default (NextJS) service.
dispatch:
- url: '*/api/*'
service: api
The problem: I've also turned on Identity-Aware Proxy for App Engine. When I try to make a GET request from my NextJS service to my API (server-side, via getServerSideProps) it triggers the IAP sign-in page again instead of hitting my API. I've tried out a few ideas to resolve this:
Forwarding all cookies in the API request
Setting the X-Requested-With header as mentioned here
Giving IAP-secured Web App User permissions to my App Engine default service account
But nothing seems to work. I've confirmed that turning off IAP for App Engine allows everything to function as expected. Any requests to the API from the frontend also work as expected. Is there a solution I'm missing or a workaround for this?
You need to perform a service to service call. That's no so simple and you have not really example for that. Anyway I tested (in Go) and it worked.
Firstly, based your development on the Cloud Run Service to Service documentation page.
You will have this piece of code in NodeJS sorry, I'm not a NodeJS developer and far least a NexJS developer, you will have to adapt
// Make sure to `npm install --save request-promise` or add the dependency to your package.json
const request = require('request-promise');
const receivingServiceURL = ...
// Set up metadata server request
// See https://cloud.google.com/compute/docs/instances/verifying-instance-identity#request_signature
const metadataServerTokenURL = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=';
const tokenRequestOptions = {
uri: metadataServerTokenURL + receivingServiceURL,
headers: {
'Metadata-Flavor': 'Google'
}
};
// Fetch the token, then provide the token in the request to the receiving service
request(tokenRequestOptions)
.then((token) => {
return request(receivingServiceURL).auth(null, null, true, token)
})
.then((response) => {
res.status(200).send(response);
})
.catch((error) => {
res.status(400).send(error);
});
This example won't work because you need the correct audience. Here, the variable is receivingServiceURL. It's correct for Cloud Run (and Cloud Functions) but not for App Engine behind IAP. You need to use the Client ID of the OAuth2 credential named IAP-App-Engine-app
Ok, hard to understand what I'm talking about. So, go to the console, API & Services -> Creentials. From there, you have a OAuth2 Client ID section. copy the Client ID column of the line IAP-App-Engine-app, like that
Final point, be sure that your App Engine default service account has the authorization to access to IAP. And add it as IAP-secured Web App User. The service account has this format <PROJECT_ID>#appspot.gserviceaccount.com
Not really clear also. So, go to the IAP page (Security -> Identity Aware Proxy), click on the check box in front of App Engine and go the right side of the page, in the permission panel
In the same time, I can explain how to deactivate IAP on a specific service (as proposed by NoCommandLine). Just a remark: deactivate security when you have trouble with it is never a good idea!!
Technically, you can't deactive IAP on a service. But you can grant allUsers as IAP-secured Web App User on a specific service (instead of clicking on the checkbox of App Engine, click on the checkbox of a specific service). And like that, even with IAP you authorized all users to access to your service. it's an activation without checks in fact.

How does Gatsby application access Azure Key Vault

I have sucessfully deployed my Gatsby App to Azure as per these instructions. This is a separate resource from a "normal" web app. The "normal web app would be deployed to the App Service resource but for Gatsby it is deployed to a different Static Web Page Reqource. So the URL now has the form https://<generated name>.azurestaticapps.net where the generated-name is NOT the application name. So it seems that the instructions for hooking the web service up with the key vault have some holes to be filled in. What are the steps that I need to take to connect my Gatsby App to my azure key vault during development and on the production site?
Error: EnvironmentCredential is not supported in the browser.
In your case, use ClientSecretCredential instead of others.
Make sure you have done the Prerequisites, then in your code, use ClientSecretCredential, pass the tenantId, clientId, clientSecret of your service principal to it, it should be something like below, retrievedSecret.value is the value of the secret.
const { ClientSecretCredential } = require("#azure/identity");
const { SecretClient } = require("#azure/keyvault-secrets");
const keyVaultName = "xxxx";
const KVUri = "https://" + keyVaultName + ".vault.azure.net";
const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
const client = new SecretClient(KVUri, credential);
const retrievedSecret = await client.getSecret(secretName);

Laravel 7 Sanctum: Same domain (*.herokuapp.com) but separate React SPA gets CSRF Token Mismatch

I've read a lot from this forum and watched a lot of tutorial videos on how to connect separate React/Vue SPA to Laravel API with Sanctum Auth but none of the solutions worked for me. This is for my school project.
So here's what I did so far.
I created 2 folders, one for api and one for frontend. I installed Laravel on the api folder and installed React app on the frontend folder. Both of these are Git initialized and have their own Github repositories. Also, both of them are deployed to Heroku.
API
Repository: https://github.com/luchmewep/jarcalc_api
Website: https://jarcalc-api.herokuapp.com
Front-end
Repository: https://github.com/luchmewep/jarcalc_front
Website: https://jarcalculator.herokuapp.com
On local, everything runs fine. I can set error messages to email and password fields on the front-end so that means I have received and sent the laravel_session and XSRF_TOKEN cookies. I have also displayed the authenticated user's information on a dummy dashboard so everything works fine on local.
On the internet, both my apps run but won't communicate with each other. In the official documentation, they must at least be on the same domain and in this case, they are subdomains of the same domain which is .herokuapp.com.
Here are my environment variables for each Heroku apps.
API
SANCTUM_STATEFUL_DOMAINS = jarcalculator.herokuapp.com
(I've tried adding "SESSION_DRIVER=cookie" and "SESSION_DOMAIN=.herokuapp.com" but still not working!)
Update
Found out that axios is not carrying XSRF-TOKEN when trying to POST request for /login. It is automatically carried on local testing.
Here is the relevant code:
api.tsx
import axios from "axios";
export default axios.create({
baseURL: `${process.env.REACT_APP_API_URL}`,
withCredentials: true,
});
Login.tsx
...
const handleSubmit = (e: any) => {
e.preventDefault();
let login = { email: email.value, password: password.value };
api.get("/sanctum/csrf-cookie").then((res) => {
api.post("/login", login).then((res) => {
/**
* goes here if login succeeds...
*/
console.log("Login Success");
...
})
.catch((e) => {
console.log("Login failed...")
});
})
.catch((e) => {
console.log("CSRF failed...");
});
};
UPDATE
".herokuapp.com is included in the Mozilla Foundation’s Public Suffix List. This list is used in recent versions of several browsers, such as Firefox, Chrome and Opera, to limit how broadly a cookie may be scoped. In other words, in browsers that support the functionality, applications in the herokuapp.com domain are prevented from setting cookies for *.herokuapp.com."
https://devcenter.heroku.com/articles/cookies-and-herokuapp-com
COOKIES ON LOCAL
COOKIES ON DEPLOYED
Explanation: Although the API and frontend both have .herokuapp.com, that does not make them on the same domain. It is explained on Heroku's article above. This means that all requests between *.herokuapp.com are considered cross-site instead of same-site.
SOLUTION
Since laravel_session cookie is being carried by axios, the only problem left is the xsrf-token cookie. To solve the problem, one must buy a domain name and set the subdomain name for each. In my case, my React frontend is now at www.jarcalculator.me while my Laravel backend is now at api.jarcalculator.me. Since they are now same-site regardless of where they are deployed (React moved to Github pages while Laravel at Heroku), the cookie can be set automatically.
Finally fixed my problem by claiming my free domain name via Github Student Pack. I set my React app's domain name to www.jarcalculator.me while I set my Laravel app's domain name to api.jarcalculator.me. Since they are now subdomains of the same domain which is jarcalculator.me, passing of cookie that contains the CSRF-token and laravel_session token is automatic. No need for modification on axios settings. Just setting the axios' withCredentials to true is all you need to do.

Resources