Authenticating AJAX API calls from the UI which is SAML authenticated - saml-2.0

We have 2 parts to our application.
The backend written in Java 8 which exposes different REST end points using Jersey 2.0.
The UI which is a Single Page Application built using React and other node modules.
The Web interface uses SAML 2.0 authentication supported by Okta as the Identity Provider. The backend creates the HTTP Session and sends the JSESSIONID in the cookie.
Now the UI calls the REST end-points to display data. We need to add an authentication layer to our REST API's and I had asked a separate question about that over here Authenticating rest endpoints and the UI using Okta.
My question here specifically is what can I pass from the UI as a means of authentication to these API calls because the UI is really authenticated and the HTTP session is still valid. So I shouldn't need to create a separate OAuth 2.0 token pass it to the UI, so that UI can pass that back to the backend. The OAuth 2.0 flow makes sense for an external client using our REST end points.
Update 1
This is the excerpt of my securityContext.xml that defines both the authentication schemes:
<!-- Authenticating REST APIs -->
<security:http pattern="/rest/**" use-expressions="false">
<security:intercept-url pattern="/nltools/**" access="IS_AUTHENTICATED_FULLY" />
<security:custom-filter before="BASIC_AUTH_FILTER" ref="authValidationFilter" />
<security:http-basic/>
</security:http>
<!-- SAML processing endpoints -->
<security:http pattern="/saml/**" entry-point-ref="samlEntryPoint">
<security:custom-filter before="FIRST" ref="metadataGeneratorFilter" />
<security:custom-filter before="CSRF_FILTER" ref="samlFilter" />
<security:custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter" />
</security:http>
<!-- Secured pages with SAML as entry point -->
<security:http entry-point-ref="samlEntryPoint" use-expressions="false">
<security:csrf />
<security:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY" />
<security:custom-filter before="FIRST" ref="metadataGeneratorFilter" />
</security:http>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="httpBasicAuthenticationProvider" />
<!-- Register authentication manager for SAML provider -->
<security:authentication-provider ref="samlAuthenticationProvider"/>
<!-- Register authentication manager for administration UI -->
<security:authentication-provider>
<security:user-service id="adminInterfaceService">
<security:user name="admin" password="admin" authorities="ROLE_ADMIN"/>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
I am not sure how can I execute SecurityContextPersistenceFilter. Should I override and add that one of the filters for my /rest/** pattern?
Update 2
Here is the JS Code (React) that makes a call to the backend:
return Promise.resolve().then(() => {
return request.post('/rest/v1/projects')
.send(data)
.then((success) => {
console.log('success!', success);
var projectName = success.body.name;
var projectId = success.body.id;
self.props.dispatch( projectActions.addNewProject(
projectId,
projectName
));
self.props.dispatch( appActions.displayGoodRequestMessage( projectName + " Saved") );
self.props.dispatch( projectActions.fetchProject( projectId ) );
self.props.router.push('/projects');
}
Now JS code can choose to send all the cookies associated with this domain and that way the backend can get the JSESSION ID cookie, however, JS doesn't do that and I don't think its the right thing to do.
If on the other hand I execute https://mydomain/rest/v1/projects in the browser and as long as I am logged in, I will get results because this time when my filter checks for the valid HTTP Session, it can get the session from the request with request.getSession(false), though that is not true when the JS calls the API. It becomes completely a different user-agent.
Update 3
Per suggestion by #Vladimír Schäfer, I could change the above JS code just a bit to send cookies as .withCredentials() and authenticate with the backend without having to do anything special
return Promise.resolve().then(() => {
return request.post('/rest/v1/projects')
.withCredentials()
.send(data)
.then((success) => {
console.log('success!', success);
var projectName = success.body.name;
var projectId = success.body.id;
self.props.dispatch( projectActions.addNewProject(
projectId,
projectName
));
self.props.dispatch( appActions.displayGoodRequestMessage( projectName + " Saved") );
self.props.dispatch( projectActions.fetchProject( projectId ) );
self.props.router.push('/projects');
}

As long as the REST API is part of the same application which front-end uses for authentication with Spring Security, it will have access to the JSESSIONID and therefore to the context of Spring Security which contains all the information about the authenticated user. Therefore there's no need for any additional authentication mechanism.
If you execute filter SecurityContextPersistenceFilter when handling your Jersey calls, you'll be able to access the security context using:
SecurityContextHolder.getContext().getAuthentication();
The SecurityContextPersistenceFilter uses its configured repository to fetch the Authentication object and stores it in the SecurityContextHolder. Have a look at the HttpContextRepository which it uses by default. There you will find that the security context is stored is HttpSession under key SPRING_SECURITY_CONTEXT, so you can also fetch it directly.
Of course you can also use Spring Security to enforce authentication and authorization on your REST API, just like you do on the front-end - then everything will be handled for you.

Related

Azure Active Directory - UI > API - 401 Error

Good Day,
Currently I have a single tenent with a React UI and .NET Core Apis secured by Azure Active Directory without any problems.
We have recently moved to a new Azure Tenent, new Active Directory etc. I have create two new App Registrations, one single App Service for UI and one for API. I have linked the App Service to AAD (UI = UI App Registration, API = API App Registration).
The problem is the API is getting a 401 error and I think see that in the original tenent the Bearer token is in a JWT format but in the new instance it's not, I believe it my be a graph api access key.
New Tenent:
Authorization: Bearer PAQABAAAAAAD--DLA3VO7QrddgJg7WevrQvEQVbZEMD8su-tIp9k2bTFUTort7SZgeDI52P6KRYefHgtmj4YrecgUKZJ2wylGuhvIzIz642n7Sg0VMU1RwKtrzWlaMqK62CaSoJcstxiEf6 *****
Orginal Tenent:
Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyIsImtpZCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyJ9.eyJhdWQiOiI3OThkN2ZkOC0zODk2LTQxOGMtOTQ0Ny0wNGFlNTQ2OGFkNDIiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83ZDE3NTU3Ni03Y2Y3LTQyMDctOTA5My0wNmNiNmQyZDIwNjAvIiwiaWF0IjoxNjE2NDUyNzExLCJuYmYiOjE2MTY0NTI3MTEsImV4cCI6MTYxNjQ1NjYxMSwiYWNyIjoiMSIsImFpbyI6IkFTUUEyLzhUQUFBQU9mejhPZHp *****
Please someone kindly enought to provide some guidance / input where I am going wrong.
Regards
Paul.
When using Azure AD to obtain an access token, an additional resource parameter is required. Otherwise, the access token is not a JWT.
For example, if your web API's application ID URI is https://contoso.com/api and the scope name is Employees.Read.All, then with oidc-client the client configuration should be :
scope: 'openid profile email Employees.Read.All',
extraQueryParams: {
resource: 'https://contoso.com/api'
}
In App Service auth configuration, you can use additionalLoginParams
"additionalLoginParams": ["response_type=code", "resource=https://contoso.com/api"]
If you did not use a custom application ID URI, it may look like
api://868662dd-3e28-4c7f-b7d5-7ec02ac9c601
Quickstart: Configure an application to expose a web API
Firstly, the scope is incorrect.
You should Expose an API in your API App Registration and then add it as a permission in your UI App Registration. You can refer to this document.
And when you try to call the 'https://login.windows.net/{tenant}/oauth2/authorize endpoint, you need to specify the scope to include api://{app id of the API App Registration}. For example: api://{app id of the API App Registration} openid profile email. Then the access token would be for calling your API.
At last, for CORS issue, please configure the CORS as * in your web app to see if it helps.
Try to follow this step: Configure App Service to return a usable access token
In my experience, this problem occurs, when you try to authorize against version 1 of the endpoint.
Instead of calling
https://login.microsoftonline.com/{tenant}/oauth2/authorize
call
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
You might be required to set something like "metadata URL" in you authorization library to:
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/.well-known/openid-configuration
Make sure your builder follows this order...lifted from our API program.cs
These must be in order of
UseRouting -> UseAuthentication -> UseAuthorisation -> MapControllers
> app.UseRouting()
> app.UseAuthentication()
> app.UseAuthorization()
> app.MapControllers()
If app.UseAuthentication and app.UseAuthorization are not in this order in statement position you Will get 401 Unauthorised as at 01/2023 .Net 6 Core.

Blazor WASM calling Azure AAD secured Functions API

I have an Azure Functions API which uses Azure Active Directory authentication. I can test locally and deployed using a browser and curl calls in a process of:
Get a code
Use the code to get a token
Pass the token to authenticate and get the function result.
I now want to call this API from my Blazor WASM app but I'm sure there must be a nice MSAL call to do all the authentication but I cannot find any documentation on what that might be.
Does anyone have a code snippet to illustrate what needs to happen?
Further Information
My Azure Functions App and Blazor WASM client are not part of the same project and are hosted on different sub-domains of Azure hypotheticalapi.azurewebsites.net and hypotheticalweb.azurewebsites.net.
The web client application registration has API Permissions for the API and the API has an application registration which exposes itself with the scope that the client app has permissions for.
Again, the API and Web app work individually. I just don't seem able to get them to talk.
I have been following the "ASP.NET Core Blazor WebAssembly additional security scenarios" documentation but after several attempts I keep coming back to the error:
Microsoft.JSInterop.JSException: invalid_grant: AADSTS65001:
The user or administrator has not consented to use the application with ID 'e40aabb0-8ed5-4833-b50d-ec7ca4e07996' named 'BallerinaBlazor5Wasm'.
Send an interactive authorization request for this user and resource.
Even though I have revoked/deleted the client's permissions on the API, it has never repeated asking for consent. Is there a way I should clear the consent I previously gave? No idea how I might do that.
This GitHub Issue appears to be relevant.
I was stuck for the last two weeks with the same error code in the same setting: Blazor WASM talking to an AAD secured Azure Functions app.
What appeared to be a problem in my case was the scopes that I was listing in the http request when contacting AAD identification provider endpoints. Almost all examples I came across use Microsoft Graph API. There, User.Read is the scope that is given as an example. My first though was that even when I am contacting my own API I have to include the User.Read scope in the request because I was reasoning that this scope is necessary to identify the user. However, this is not the case and the only scope that you have to list when you call the authorize and token endpoints is the one that you exposed under the "Expose an API blade" in your AAD app registration.
I am using the OAuth2 authorization code in my example and not the implicit grant. Make sure that in the manifest of your API registration you have set "accessTokenAcceptedVersion": 2 and not "accessTokenAcceptedVersion": null. The latter implies the use of implicit flow as far as I know.
The scope the I exposed in my API is Api.Read. You can expose more scopes if you need but the point is that you only ask for scopes that you exposed.
I also have both following options unticked (i.e. no implicit flow). However, I tried with selecting "ID token" and it still worked. Note that the "ID token" option is selected by default if you let the Azure Portal create your AAD app registration from your function app Authentication blade.
Blazor code
Program.cs
This code has to be added.
builder.Services.AddScoped<GraphAPIAuthorizationMessageHandler>();
builder.Services.AddHttpClient("{NAME}",
client => client.BaseAddress = new Uri("https://your-azure-functions-url.net"))
.AddHttpMessageHandler<GraphAPIAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("{NAME}"));
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
// NOTE: no "api://" when providing the scope
options.ProviderOptions.DefaultAccessTokenScopes.Add("{you API application id}/{api exposed scope}");
});
appsetting.json
"AzureAd": {
"Authority": "https://login.microsoftonline.com/{aad tenant id}",
"ClientId": "{application id of your blazor wasm app}",
"ValidateAuthority": true
}
GraphAPIAuthorizationMessageHandler.cs
Note that this class can have a different name. you'll then also reference a different name in Program.cs.
public class GraphAPIAuthorizationMessageHandler : AuthorizationMessageHandler
{
public GraphAPIAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "https://your-azure-functions-url.net" },
// NOTE: here with "api://"
scopes: new[] { "api://{you API application id}/{api exposed scope}" });
}
}
I hope this works. If not, let me know.
At least you need to get the access token, then use the token to call the function api. In this case, if you want to get the token in only one step, you could use the client credential flow, MSAL sample here, follow every part on the left to complete the prerequisites.
The following are the approximate steps(for more details, you still need to follow the sample above):
1.Create a new App registration and add a client secret.
2.Instantiate the confidential client application with a client secret
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithClientSecret(config.ClientSecret)
.WithAuthority(new Uri(config.Authority))
.Build();
3.Get the token
string[] scopes = new string[] { "<AppId URI of your function related AD App>/.default" };
result = await app.AcquireTokenForClient(scopes)
.ExecuteAsync();
4.Call the function API
httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
// Call the web API.
HttpResponseMessage response = await _httpClient.GetAsync(apiUri);
...
}

How to access a web api secured with Azure AD B2C using react js

I am having a .net core web api which is secured with Azure AD B2C and I am trying to access the api using react js msal library.
new Msal.UserAgentApplication(config.applicationId,
authority,
authCallback,
{ logger: logger,
cacheLocation: config.cacheLocation,
postLogoutRedirectUri: config.postLogoutRedirectUri,
redirectUri: config.redirectUri }
);
How to call a web api by passing the JWT token by using react js
It looks like you are using MSAL js v0.2.3 or v0.2.4, the new MSAL v1.0.0 release takes a configuration object in the constructor for the UserAgentApplication. Make sure to refer to the documentation for correct usage for your version:
https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki
Regardless - you'll need to initiate a login after initialization by calling loginPopup or loginRedirect on the MSAL app. After successful credentials are entered and you return to your web app, you can call MSAL's getUser() ( or getAccount() if using v1.0.0 )
After that, you'll need to get an access token by calling acquireTokenSilent(scopes), where the scopes array is defined by your B2C application.
( In v1.0.0 you'd pass a parameter object like this: { scopes: [scopes] } )
Once you have an access token - you'll need to pass it as the Authorization header on your REST request, and then validate it in your WebAPI by calling back to B2C before processing the request.
This is an example of the validation process:
https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-webapi-manual-jwt-validation/

With ADAL.js use of ClientId as Audience in Endpoints for SPA config

I am creating an AngularJS client to interact with two ASP.Net Core (v2.0) APIs, all of which are to be secured with Azure AD. Because of a requirement to use roles and groups we will be using the v1 endpoint and therefore ADAL.js. The UI client must be a separate project from each API, additionally the UI project is not a Visual Studio project, rather VSCode with npm. In the UI project I am using:
AngularJS 1.6.9
UI-Router 1.0.15
ADAL.js 1.0.17
After a long time of trial and error, I finally got the UI to authenticate to the API after I took the following steps:
In the UI project I included endpoints in the Adal init() function:
var endpoints = {
'http://localhost:8000/api0/': '<API_0_CLIENT_ID HERE>',
'http://localhost:8001/api1/': '<API_1_CLIENT_ID HERE>',
};
adalAuthenticationServiceProvider.init(
{
tenant: 'slurm.onmicrosoft.com',
clientId: '00000000-0000-0000-0000-XXXXXXXXXX',
endpoints: endpoints
},
$httpProvider
);
There are two endpoints, one is the base url for each API, and each has the corresponding clientId for that API as assigned when each api was registered in Azure AD. Also, the UI project, when registered in Azure AD, is granted appropriate permissions to each api. Once I did set these endpoints in the init() function, and the UI had each clientId of the APIs, the UI was able to authenticate properly to both APIs. This is the SO question that finally clued me in to try this: 32352325
If I do not provide the endpoints in the UI project, a token is not even passed back to the API and therefor authentication fails.
What I am not sure of (there is no clear documentation), is whether the UI clientId should be set as the audienceId in each API or keep each API client id embedded in the UI.
Questions:
1) For an AngularJS UI project that is separate from each API, and each project is registered separately in Azure AD, do we register the UI clientId as an audience with each API or allow the client to know each API clientId?
2) Why are endpoints seemingly required to be specified in the ADAL init() function on the client? Or am I using them incorrectly?
Based on the ReadMe of this Azure-Samples project, it would appear the UI should know the clientId of each API (under Step 3, Configure the WebApp it says):
In the TodoListWebApp project, //...// Find the TodoListResourceId property and replace the value with the Application ID of the TodoListService app
But, this example is not an SPA example and therefore does not use the implicit flow.
Much thanks in advance!
This is what I eventually deduced (after lots of digging and trial and error). Again the example assumes the UI is a separate project from any APIs. For the original questions above:
1) For an AngularJS UI project that is separate from each API, and
each project is registered separately in Azure AD, do we register the
UI clientId as an audience with each API or allow the client to know
each API clientId?
The Client UI knows about each API but uses the App ID URI
2) Why are endpoints seemingly required to be specified in the ADAL
init() function on the client? Or am I using them incorrectly?
Read on.
For each API your UI will access, there needs to be an endpoint declared in the endpoints map structure. For example, say I am trying to access 2 separate APIs registered in AAD with the following relevant info:
Tenant: slurm.onmicrosoft.com
API_0
Home Page URL: 'https://localhost:8000'
App ID URI: 'https://slurm.onmicrosoft.com/00000000-0000-0000-0000-aaaaaaaaaaaa'
API_1
Home Page URL: 'https://localhost:4000'
App ID URI: 'https://slurm.onmicrosoft.com/00000000-0000-0000-0000-bbbbbbbbbbbb'
adalAuthenticationServiceProvider.init(
{
tenant: 'slurm.onmicrosoft.com',
clientId: '00000000-0000-0000-0000-XXXXXXXXXX',
endpoints: {
'localhost:8000': 'https://slurm.onmicrosoft.com/00000000-0000-0000-0000-aaaaaaaaaaaa',
'localhost:4000': 'https://slurm.onmicrosoft.com/00000000-0000-0000-0000-bbbbbbbbbbbb',
}
},
$httpProvider
);
The following are the references:
Go to http://www.cloudidentity.com/blog/2015/02/19/introducing-adal-js-v1
Scroll to section Calling API via CORS
Follow the link to the github example:
https://github.com/Azure-Samples/active-directory-angularjs-singlepageapp-dotnet-webapi
On the github example readme
Go to Step 3: Configure the To Go API to use your Azure Active
Directory tenant
See step #7 which states:
Enter a mapping of the To Go API endpoint location to its resource
identifier, or App ID URI. The name of the property of the endpoints
object should be the location of the To Go API.
Also if you look into the code example you will see in the web.config of the ToGoAPI it indicates the 'Audience' value is set to the App ID URI of the ToGoAPI
Note: The App Id URIs used above are intentionally left in a format similar to the default values Azure AD will
provide when you register an application. These can be changed (just make sure you change it everywhere).
Note 2: In the endpoints map you see that the keys do not include the scheme and to not fully match the corresponding Home Page URL. When I included the scheme i.e https:// I got 401 responses from the API

MVC with Angular - ValidateAntiForgeryToken fails after Azure Login redirect with Adal

I have an MVC site with an embedded angular client and I've recently implemented an anti forgery XSRF token as a security measure.
I have set it up in Startup.cs as follows:
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
app.Use(next => context =>
{
if (string.Equals(context.Request.Path.Value, "/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.Request.Path.Value, "/index.html", StringComparison.OrdinalIgnoreCase))
{
// We can send the request token as a JavaScript-readable cookie, and Angular will use it by default.
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
return next(context);
});
And I've implemented it within my angular front-end like so:
{ provide: XSRFStrategy, useFactory: xsrfFactory}
export function xsrfFactory(): CookieXSRFStrategy {
return new CookieXSRFStrategy('XSRF-TOKEN', 'X-XSRF-TOKEN');
}
And protecting my controllers like:
[Authorize] //Validation of AzureAD Bearer Token.
[ValidateAntiForgeryToken]
public class UserController : Controller
It is intended that the X-XSRF-TOKEN header be validated with any call to my API, and this works successfully for all calls in the original session. However, my app uses Adal to log the user in, and after the redirect from a successful login, this validation step fails and I receive a 400 from my API for any subsequent calls.
The original X-XSRF-TOKEN header is still sent with all outgoing requests from my angular client after the login so I suspect it must be that my server side no longer has the token to validate against, or my server has generated a new one and my client doesn't retrieve it. But for whatever reason it breaks down and it's very hard to debug without creating some custom filter so I can see what's going on inside it.
Is there a way to reset this token after a client side redirect so that both my server and client share common knowledge of it again? Or should I be generating the token in my Index.html for example?
EDIT
Edited controller decoration above for missing [Authorize] attribute.
So my controller is protected by a step validating the AzureAD Bearer token as well as the Anti-Forgery validation. Removing the AzureAD Validation as a test did not resolve the issue, oddly.
Error on failing API calls displays in output after Adal login as:
The provided anti-forgery token was meant for a different claims-based user than the current user.
Based on my understanding, you were protecting the controller using token. For this issue is expected, you can refer the progress of validate the anti-XSRF tokens from below(refer this link):
To validate the incoming anti-XSRF tokens, the developer includes a ValidateAntiForgeryToken attribute on her MVC action or controller, or she calls #AntiForgery.Validate() from her Razor page. The runtime will perform the following steps:
The incoming session token and field token are read and the anti-XSRF token extracted from each. The anti-XSRF tokens must be identical per step (2) in the generation routine.
If the current user is authenticated, her username is compared with the username stored in the field token. The usernames must match.
If an IAntiForgeryAdditionalDataProvider is configured, the runtime calls its ValidateAdditionalData method. The method must return the Boolean value true.
Since you were developing the SPA application with back-end web API, when the request to the web API, it will always issue the anti-XSRF token with no identity. And when you send the request to the back-end with anti-XSRF and Azure AD token, this time the web API already authenticate the request via the Azure AD token. And it will always return false when checking anti-XSRF token to match the identity information.
In this scenario, if the back-end only using the bear token authentication and store the token with session storage, there is no need to enable XSRF prevention since others is not able to steal the token and forge the request.
If your back-end also support the cookie authentication or basic auth, NTLM etc, you can disable the identity checking by adding the following to your Application_Start method: AntiForgeryConfig.SuppressIdentityHeuristicChecks = true.(refer this link)
More detail about XSRF/CSRF abouth oauth and web API, you can refer the threads below:
How does ValidateAntiForgeryToken fit with Web APIs that can be accessed via web or native app?
AntiForgeryToken does not work well with OAuth via WebAPI
Try replacing [ValidateAntiForgeryToken] with [AutoValidateAntiforgeryToken]
https://github.com/aspnet/Antiforgery/blob/dev/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs

Resources