REACT application to call secure Azure WEBAPI Service - NO USERS - reactjs

I have created a simple REACT application that is ONLY run on a local PC attached to a large screen on our network. Internal use only! It is like a billboard or dashboard. There is ZERO user interaction. The screen is NOT a touch screen and there is no keyboard and mouse attached. Therefore NO users to login.
The REACT application is build and then deployed to a folder on the PC. All automated. The initial deployment includes all current data. Then at windows startup a command something like this is executed:
"python -m http.server 3000" (just example...)
The application has initial data that was deployed with the application, however, I would like it to also be able to call a secure Azure WebAPI service to get updated statistics every few minutes. Very small data. Mostly integer values. I just want to provide some real time updates.
I have the REACT app fully working (if the WEBAPI is not secure) or the individual calls allow anonymous. However, we have business rules that require all endpoints to be secure.
This app runs locally, but the API is an Azure App Service.
I have setup the REACT application in Azure AD as a registered application and configured it to have permissions to call the WEBAPI service.
I have many console applications that are setup and work basically the same way as this REACT application. With the C# daemon applications, there is a MSAL package that makes it easy.
I am trying to learn REACT, and instead of building this as another WPF or UWP application, I wanted to try using REACT.
So, I know I need an access token somehow. I was thinking with a client ID and Secret just like I do in my C# daemon clients that are written in C#.
I cannot find any REACT nor Angular examples that do this without a user login first. Remember, the PC does not have input devices. Display ONLY. Again, my app does not have users. It calls a secure API to get data. That's it.
Thanks for your help.
Using Joy Wang's comments and this page from documentation:
Service-to-Service Access Token Request
This is my new code:
const adalConfig = {
tenant: '...',
clientId: '...',
clientSecret: '...',
authority: 'https://login.microsoftonline.com/{tenant}/oauth2/token',
endpoints: {
apiResourceId: 'api://bbbbbb-...',
},
};
function getAccessToken() {
var requestParams = {
grant_type: 'client_credentials',
client_id: adalConfig.clientId,
client_secret: adalConfig.clientSecret,
resource: adalConfig.endpoints.apiResourceId
};
// Make a request to the token issuing endpoint.
fetch(adalConfig.authority,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( requestParams )
}).then(response => {
if (response.status >= 200 && response.status < 300) {
console.log(response);
console.log(response.json());
} else {
console.log('Somthing happened wrong');
console.log(response);
}
}).catch(err => err);
}
When I call the function above, I get the following response:
Response {type: "cors", url: "https://login.microsoftonline.com/.../oauth2/token", redirected: false, status: 400, ok: false, …}
body: (...)
bodyUsed: false
headers: Headers {}
ok: false
redirected: false
status: 400
statusText: "Bad Request"
type: "cors"
url: "https://login.microsoftonline.com/.../oauth2/token"
proto: Response
Maybe there is another way to start the REACT application so that CORS is not checked? Any ideas?
Thanks again.

So, currently there is not a secure way to do what I want. The basic issue is that you cannot use the client credential grant type from JavaScript in a browser.
However, I think I have a good work around that may help others. I am sure it is NOT for most application. And I believe OAUTH is working on a solution so this may not be needed in the near future. If a better solution is add, I will gladly mark it as the correct answer. Thanks for your help.
My app is basically an automated dashboard/billboard with ZERO user input. It pulls secure data and displays it. The REACT application is ONLY on a LOCAL PC on a wall with NO inputs. A script runs when the PC is turned on.
The script starts the built REACT application using an http server like python.
Ex: "python -m http.server 8000"
The script then opens the browser in kiosk mode so the only thing you see on the screen is the application.
So far, this is exactly as I had it before.
WORK AROUND:
I created a command line utility called GetToken. Before the REACT application is started by the script, it calls this utility like so: "gettoken --client Dashboard --file token.json"
This utility makes the Client Credential Grant Type call to get a token.
It then saved that token to a local json file with the other built REACT files. Ex: \public\data\token.json
In my REACT application, it just loads the token and uses it.
const t = await fetch('./data/token.json').then(r => r.json());
this.setState({ token: t.token });
Then I just add this to my api calls like so:
const fetchOptions = {
method: 'GET',
headers: {
"Authorization": `Bearer ${this.state.token}`,
"Content-Type": "application/json"
}
};
const newSlides = await fetch(this.state.baseUrl + '/api/Dashboard/GetSlides', fetchOptions).then(response => response.json());
IMPORTANT: This only works if you also have the ability to update the API. If you cannot, then you will still get CORS errors. You will have to allow calls from the localhost and port you use to start you application. You should pick something other than 3000, 4200, or 8000.
I added the following to my API startup.cs:
public void ConfigureServices(IServiceCollection services) {
...
var origins = Configuration.GetSection("AppSettings:AllowedOrigins").Value.Split(",");
services.AddCors(o => o.AddPolicy(specificOriginsPolicy, builder => {
builder.WithOrigins(origins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.SetIsOriginAllowed((host) => true);
}));
...
}
public void Configure(IApplicationBuilder app) {
...
app.UseCors(specificOriginsPolicy);
...
}
I am still refining this solution, but it works well so far. I may turn the utility into a background service that is updating the token on an interval. Or I may turn the utility into a Shell, and then use it instead of the script. Either way, you get the idea.
LESSON:
I know I could have done this as a UWP or WPF application and avoided all these issues, but the main goal was to learn REACT. I learned a lot. I would do it again. It is shocking just how little code there is to my REACT application now that it is done. I believe REACT could be used for many similar scenarios.

You could refer to this sample, it uses client credential flow(i.e. client id and secret you want) to get the access token, just change the resource to the one you want to get token for, the sample gets the token for Microsoft Graph.
auth.getAccessToken = function () {
var deferred = Q.defer();
// These are the parameters necessary for the OAuth 2.0 Client Credentials Grant Flow.
// For more information, see Service to Service Calls Using Client Credentials (https://msdn.microsoft.com/library/azure/dn645543.aspx).
var requestParams = {
grant_type: 'client_credentials',
client_id: config.clientId,
client_secret: config.clientSecret,
resource: 'https://graph.microsoft.com'
};
// Make a request to the token issuing endpoint.
request.post({ url: config.tokenEndpoint, form: requestParams }, function (err, response, body) {
var parsedBody = JSON.parse(body);
console.log(parsedBody);
if (err) {
deferred.reject(err);
} else if (parsedBody.error) {
deferred.reject(parsedBody.error_description);
} else {
// If successful, return the access token.
deferred.resolve(parsedBody.access_token);
}
});
return deferred.promise;
};

Related

Error with authentication in e2e tests using cypress: chrome-error://chromewebdata

I'm using cypress for writing E2E tests for my UI (Note that it's a PowerBI report, hence it's kind of special case). When I am testing with a public report, it works fine. But when it's a private PBI report, I am having trouble with login part. After some research, I found this approach promising for Azure AD based auth, and added this login function in my commands.js file:
Cypress.Commands.add('login', () => {
cy.request({
method: 'POST',
url: 'https://login.microsoftonline.com/{TENANT}/oauth2/token',
form: true,
body: {
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
// resource: RESOURCE
},
header: {
'Content-Type': 'multipart/form-data'
}
}).then((responseData) => {
if (responseData.status === 200) {
window.sessionStorage.setItem("adal.idtoken", responseData.body.access_token);
window.sessionStorage.setItem("adal.token.keys", CLIENT_ID + "|")
window.sessionStorage.setItem(`adal.expiration.key${CLIENT_ID}`, responseData.body.expires_on)
window.sessionStorage.setItem(`adal.access.token.key${CLIENT_ID}`, responseData.body.access_token)
} else {
console.log("error retrieving token")
}
})
})
Note that the Client ID and secret are correct and have permission to access the powerbi report. I also tested the token generated, and the sessionStorage variables, and all seem to be assigned correctly. Now, in my test:
describe("E2E Tests", () => {
beforeEach(() => {
cy.login();
})
it("Sample Test 1", () => {
cy.visit("https://powerbi-report-url.com");
//...
});
})
And I am seeing in the cypress test runner that, even though login has been called in beforeEach, while visiting the powerbi report, it still redirects to https://login.microsoftonline.com url with a different client id as query param, and since the superdomains of powerbi report and redirected urls are different, it gives chrome-error://chromewebdata error(I guess that's the reason). Hence wondering, how to login to a website in cypress tests backed by azure ad auth.
Also, might be unrelated, but seeing one more error in the console:
Refused to display 'https://powerbi-report-url.com' in a frame because it set 'X-Frame-Options' to 'deny'.
Edit 1: Switching to Edge doesn't give the chrome webdata error, but still the cy.visit to the URL times out and gives HTTP 431 Error(Request header too long) and couldn't authenticate.
Edit 2 (More Details about Auth Error): While generating the toke using client credentials, I am getting the token, and see it's stored in the session Storage, however the cypress tests are not picking the same token to authorize the visit to PowerBI report. So, basically even thought the cookie exist to auth the request, the request to Power BI visit still redirects to login.microsoftonline.com/common/oauth2/authorize?client_id={a different client ID from what I am using in the above POST call}
Whereas, while using username/password; getting this error: "error": "interaction_required", "error_description": "AADSTS50079: Due to a configuration change made by your administrator, or because you moved to a new location, you must enroll in multi-factor authentication to access
"error_codes": [50079]
At this moment (17-Apr-20), this might be related to an Open issue with the Cypress team: https://github.com/cypress-io/cypress/issues/4220
For me particularly, I used to have one super-domain having this error with the previous version 4.3.0 but now with 4.4.0, I get more domains having same issue.
Current workaround: Roll back to previous version and run via Edge (which is based on Chromium anyway).

How to pass windows authentication(browser) from react application to Spnego Kerberos Spring SSO?

We have a react application which used get that data from spring boot webservice. Both is deployed in a same server(tomcat). But we only need Kerberos authentication for webservice call from the React application. Anyone can open the React application but when it navigate then it calls to the webservcie to get the data. So if we configure the spring to support spnego kerberos spring sso, is it possible that browser will automatically pass( from React app, as react run on the browser) the logged in Windows credentials to the spring boot web service.
We are calling the service from react app as follows -
export const client = rest
.wrap(mime, { registry: registry })
.wrap(errorCode)
.wrap(defaultRequest, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
method: 'GET'
})
export const fetchPDSIs = (Id) =>
APIHelpers.client(APIHelpers.buildPDSIReq(Id))
.then(
response => (response.entity || []).sort((a, b) => a.portalinstance.localeCompare(b.portalinstance))
,
response => {
global.msg.error(<div className='smallTextNotification'>`Fetching instances and portal for {Id} error: {response.status.code} -> {response.status.text}</div>)
return []
}
)
export const buildPDSIReq = (Id) => ({path: `${serverAddr}/msd232/pdsiii/${Id}`})
Using the fetch API, it worked for me by adding credentials: 'include'
fetch(${authBase}/auth, {credentials: 'include'})
I know it isn't what you are using but it may help other readers
Yes, it's possible, requirements on the client side:
User logged into domain account on OS.
Proper config in your browser, see Spring documentation
E.g. for Internet Explorer:
E.3 Internet Explorer
Complete following steps to ensure that your Internet Explorer browser
is enabled to perform Spnego authentication.
Open Internet Explorer.
Click Tools > Intenet Options > Security tab.
In Local intranet section make sure your server is trusted by i.e. adding it into a list.
Kerberos auth is triggered by HTTP header returned from backend service:
WWW-Authenticate: Negotiate
If your OS and browser are correctly configured this will trigger service ticket generation, which browser will send as Authorization HTTP header value.
EDIT:
If your application is split into frontend and backend hosted separately on different hosts, then you have to register SPN (and generate keytab) for the fronted host which users will enter. Example:
Backend: api.test.com
Frontend: application.test.com
For SSO to work, you have to register SPN: application.test.com, backend host name is irrelevant here. Command:
setspn -A HTTP/application.test.com#test.com ad_user_to_registern_spn_for

Azure Web App with Acitve Directory Express with Graph API to get user photo

My Azure Web App has Active Directory enabled using the Express option. I can get the user claims/user's name from auth.me. How do I then get the user's photo/avatar? The token I get is not working in a Graph API call. I get this error from Graph API. Here is my code.
Please help! Spent hours searching and reading docs but nothing seems to address the Express AD scenario.
Thanks
Donnie
{
"error": {
"code": "InvalidAuthenticationToken",
"message": "CompactToken parsing failed with error code: 80049217",
"innerError": {
"request-id": "e25f1fe5-4ede-4966-93c2-6d92d34da6ae",
"date": "2019-03-13T14:13:26"
}
}
}
axios.get('/.auth/me').then(resp => {
if(resp.data){
loggedInUser = {
accessToken:resp.data[0].access_token,
userId: resp.data[0].user_id,
username: resp.data[0].user_claims[9].val,
lastname: resp.data[0].user_claims[8].val,
fullname: resp.data[0].user_claims[11].val,
avatar:'https://cdn.vuetifyjs.com/images/lists/1.jpg'
}
let config = {
'headers':{
'Authorization': 'Bearer ' + loggedInUser.accessToken
}
}
axios.get('https://graph.microsoft.com/v1.0/me/photos/48x48/$value',config).then(resp => {
let photo = resp.data;
const url = window.URL || window.webkitURL;
const blobUrl = url.createObjectURL(photo);
document.getElementById('avatar').setAttribute("src", blobUrl);
loggedInUser.avatar = blobUrl;
console.log(blobUrl)
});
}
})
I was able to pull the image using MSDAL to handle the token. The new App Registration blade (as of 4/10/2019 is in preview) has a quick start which will ensure your app registration is correctly configure and allow you to download sample code.
In this blade, make sure you've added graph API permissions as shown below. When you click on Quick Start, you'll get a sample similar to this gist. It makes use of MSAL js library which handles the token negotiation.
var myMSALObj = new Msal.UserAgentApplication(applicationConfig.clientID, applicationConfig.authority,
acquireTokenRedirectCallBack, {
storeAuthStateInCookie: true,
cacheLocation: "localStorage"
});
function signIn() {
myMSALObj.loginPopup(applicationConfig.graphScopes).then(function (idToken) {
//Login Success
showWelcomeMessage();
acquireTokenPopupAndCallMSGraph();
}, function (error) {
console.log(error);
});
}
After that, the magic happens in acquireTokenPopupAndCallMSGraph() which will acquire the token so you can use it to call the graph API. Now my gist makes use of XMLHttpRequest which I'm sure you'll be able to replace with axios.
To get the photo in the v1.0, it supports only a user's work or school mailboxes and not personal mailboxes.
For the details, you could read here.
Your AD app registration may not have the necessary delegate permissions. To add those permissions to your app, see these steps. I think you may need to use the oauth (login.microsoftonline.com/{{tenant}}/oauth2/v2.0/token) endpoint rather than .auth/me. With the oauth endpoint, you can even elect to pass in the scopes your token needs for calling the graph API. You can use http://jwt.ms to decode the token and see if has the necessary delegate permissions.
Also, I came across this blog series that lists various tutorials for working the Microsoft Graph. You can also check out https://github.com/microsoftgraph/nodejs-apponlytoken-rest-sample.
Furthermore, https://github.com/microsoftgraph/nodejs-connect-rest-sample makes use of passport and passport-azure-ad npm packages. That actually may be more advantagous to getting and managing tokens from Azure AD.
Hope this helps.
Ryan, I added delegate permissions to my web app's permissions settings for reading user profiles, but I still get the error message when tying to get profile pic from graph. Not sure what permissions it needs, but I basically gave it full access to use's profile. Graph just doesn't seem to like the token provided by AD Express config (login.microsoftonline.com)
{
"error": {
"code": "InvalidAuthenticationToken",
"message": "CompactToken parsing failed with error code: 80049217",
"innerError": {
"request-id": "e25f1fe5-4ede-4966-93c2-6d92d34da6ae",
"date": "2019-03-13T14:13:26"
}
}
}
Ryan, jwt fails when I paste the full token from auth/me .

What is the best practice to use Oauth2, React, Node.js and Passport.js to authenticate user with Google sign on button?

I want to have a login button in my website so when a user clicks on it, the user can use their Google credentials. I'd like to ideally perform the authentication server side using Express.js and Passport.js.
I implemented authentication server-side but the problem is that I can't make an AJAX request from the website to the server to start authentication because Google or Oauth don't support CORS. So I need to use a href element in my website which would call the server authentication endpoint. However, I can't catch server response in this way.
If I perform the authentication client-side (I'm using React) I could store login state in Redux and allow the user to access the website's resources. However, when the user logs out I need to make sure that server endpoints stop serving the same user which feels like implementing authentication twice: client-side and server-side.
In addition when authenticating client-side, Google opens a popup for the user to authenticate which I think is worse user experience then just a redirect when authenticating server-side.
I'm wondering what the best practice in terms of authenticating using Oauth2/Google. For example, stackoverflow.com also has Google button but just makes a redirect, without any popup, so I guess they figured out a way to perform server-side authentication and to bypass CORS issue.
I faced the same issue. This article is Gold link
1.In auth route File I had following code
const CLIENT_HOME_PAGE_URL = "http://localhost:3000";
// GET /auth/google
// called to authenticate using Google-oauth2.0
router.get('/google', passport.authenticate('google',{scope : ['email','profile']}));
// GET /auth/google/callback
// Callback route (same as from google console)
router.get(
'/google/callback',
passport.authenticate("google", {
successRedirect: CLIENT_HOME_PAGE_URL,
failureRedirect: "/auth/login/failed"
}));
// GET /auth/google/callback
// Rest Point for React to call for user object From google APi
router.get('/login/success', (req,res)=>{
if (req.user) {
res.json({
message : "User Authenticated",
user : req.user
})
}
else res.status(400).json({
message : "User Not Authenticated",
user : null
})
});
2.On React Side After when user click on button which call the above /auth/google api
loginWithGoogle = (ev) => {
ev.preventDefault();
window.open("http://localhost:5000/auth/google", "_self");
}
3.This will redirect to Google authentication screen and redirect to /auth/google/callback which again redirect to react app home page CLIENT_HOME_PAGE_URL
4.On home page call rest end point for user object
(async () => {
const request = await fetch("http://localhost:5000/auth/login/success", {
method: "GET",
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"Access-Control-Allow-Credentials": true,
},
});
const res = await request.json();
//In my case I stored user object in redux store
if(request.status == 200){
//Set User in Store
store.dispatch({
type: LOGIN_USER,
payload : {
user : res.user
}
});
}
})();
5.last thing add cors package and following code in server.js/index.js in node module
// Cors
app.use(
cors({
origin: "http://localhost:3000", // allow to server to accept request from different origin
methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
credentials: true // allow session cookie from browser to pass through
})
);
Your authentication should be done server side. Here is how it works.
You make a fetch or axios call to your authentication route.
Your authentication route sends a request to Google's Authentication servers. This is important to have on the backend because you will need to provide your clientSecret. If you were to store this on the frontend, it would make it really easy for someone to find that value and compromise your website.
Google authenticates the user and then sends you a set of tokens to your callback url to use for that user (refresh, auth, etc...). Then you would use the auth token for any additional authorization until it expires.
Once that expires, you would use the refresh token to get a new authorization token for that client. That is a whole other process though.
Here is an example of what that looks like with Passport.js: https://github.com/jaredhanson/passport-google-oauth2
EDIT #1:
Here is an example with comments of the process in use with Facebook, which is the same OAuth codebase:
https://github.com/passport/express-4.x-facebook-example/blob/master/server.js
Redux can really help with achieving this and this follows the same logic as Nick B already explained...
You set up oauth on the server side and provide an endpoint that makes that call
You set up the button on you react frontend and wire that through an action to the endpoint you already setup
The endpoint supplies a token back which you can dispatch via a reducer to the central redux store.
That token can now be used to set a user to authenticated
There you have it.

Bad Request (400) When Trying to Authenticate Harvest API in React

So I'm building a status board for our internal use as developers here in the office. It will show number of commits, hours tracked, etc.
I am following this model for authenticating. After a user logs in with harvest it redirects them back to the app with the code param as a query string, I'm then taking that query string and passing it to a state to then do a fetch to get the access token (so that I can later pull API data).
What happens, is the login is successful but when you are redirected back to the app the fetch throws a Bad Request (400) error. I have tested in Postman and can get the correct response so I'm not sure what the issue is...
Here is some of the JS from the Main component that sets the states if there is a code param:
harvestState() {
// grab URL params
let urlParams = queryString.parse(location.search);
console.log(urlParams);
console.log(urlParams.code);
// set the state based on the paramater passed back
urlParams.code ? (
this.setState({
harvestcode: urlParams.code
})
) : (
this.setState({
harvestcode: 'none'
})
);
}
componentWillMount(){
this.harvestState();
}
And here is the fetch function in my Harvest component:
getHarvest(){
const clientSecret = 'XXXXXXXXXX';
// Set Harvest Headers
const harvestHeaders = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
mode: 'no-cors',
body: {
'code': this.props.code,
'client_id': this.props.clientid,
'client_secret': clientSecret,
'redirect_uri': 'http://dash.mycompany.me',
'grant_type': 'authorization_code'
}
};
fetch('https://mycompany.harvestapp.com/oauth2/token', harvestHeaders)
.then( response => response.json() )
.then( token => {
console.log(token);
} )
}
componentDidMount(){
if( this.props.code !== 'none' ){
this.getHarvest();
}
}
Is there something here that I am doing wrong? Why does it always return a bad request? Any help would be appreciated. Thank you!
At least one issue you have is that when you use mode: 'no-cors' you’re telling the browser to handle the response as an opaque response, which means that you’re telling the browser to not make any properties of the response object accessible from JavaScript.
So if you make a mode: 'no-cors' request, response => response.json() is going to fail.
The only purpose for no-cors in practice is in combination with Service Workers when you’re just caching resources (e.g., images) from responses, without need to get properties of the responses.
Anyway, given that the client Web app making the requests in your deployment is running from a different origin than the server the requests are sent to, browsers are going to block the requests unless the server responds with the necessary CORS headers—Access-Control-Allow-Origin, for a start. For an explanation, see the MDN article HTTP access control (CORS).
That is, browsers block cross-origin requests made from JavaScript unless the server the requests are sent to opts-in to allowing those, with the Access-Control-Allow-Origin, etc., response headers. The reason Postman doesn’t block such requests is that Postman is not an arbitrary Web app running at some specific origin on the Web but is instead a browser plugin that you’ve intentionally installed. So it’s not bound the cross-origin restrictions browser enforce for Web apps.

Resources