Dotnet API requires auth both for application and React - reactjs

I must be really stupid, But I have been struggling for weeks to try solve this issue, and all the digging I have done (in Stack overflow and MS Documentation) has yielded no results (or I'm too stupid to implement auth correctly)
I have a dotnet service which needs to act as an API - both for an application to post data to (an exe which logs exception data), and for a UI (react app) to get the posted exceptions
the exe can successfully send data to the dotnet app after first getting a token from login.microsoftonline.com and then sending the token (and secret) in the http request.
A sample postman pre-request script of the auth used (I've set all the secret stuff as environment variables):
pm.sendRequest({
url: 'https://login.microsoftonline.com/' + pm.environment.get("tenantId") + '/oauth2/v2.0/token',
method: 'POST',
header: 'Content-Type: application/x-www-form-urlencoded',
body: {
mode: 'urlencoded',
urlencoded: [
{key: "grant_type", value: "client_credentials", disabled: false},
{key: "client_id", value: pm.environment.get("clientId"), disabled: false},
{key: "client_secret", value: pm.environment.get("clientSecret"), disabled: false}, //if I don't configure a secret, and omit this, the requests fail (Azure Integration Assistant recommends that you do not configure credentials/secrets, but does not provide clear documentation as to why, or how to use a daemon api without it)
{key: "scope", value: pm.environment.get("scope"), disabled: false}
]
}
}, function (err, res) {
const token = 'Bearer ' + res.json().access_token;
pm.request.headers.add(token, "Authorization");
});
Now in React, I am using MSAL(#azure/msal-browser) in order to login a user, get their token, and pass the token to one of the dotnet endpoints using axios as my http wrapper, but no matter what I do, it returns http status 401 with WWW-Authenticate: Bearer error="invalid_token", error_description="The signature is invalid".
A simplified code flow to login user and request data from the API:
import {publicClientApplication} from "../../components/Auth/Microsoft";//a preconfigured instance of PublicClientApplication from #azure/msal-browser
const data = await publicClientApplication.loginPopup();
// ... some data validation
publicClientApplication.setActiveAccount(data.account);
// .. some time and other processes may happen here so we don't access token directly from loginPopup()
const activeAccout = publicClientApplication.getActiveAccount();
const token = publicClientApplication.acquireTokenSilent(activeAccount).accessToken;
const endpointData = await api()/*an instance of Axios.create() with some pre-configuration*/.get(
'/endpoint',
{ headers: {'Authorization': `bearer ${token}`} }); // returns status 401
The dotnet service has the following configurations
public void ConfigureServices(IServiceCollection services){
...
var authScheme = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
authScheme.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
...
}
namespace Controllers{
public class EndpointController : ControllerBase{
...
[Authorize]
[HttpGet]
public IActionResult GetEndpoint(){
return Ok("you finally got through");
}
}
}
I've literally tried so many things that I've lost track of what I've done...
I've even cried myself to sleep over this - but that yielded no results
i can confirm that running the request in postman, with the pre request script, it is possible to get the response from the endpoint

So....
After much digging and A-B Testing I was able to solve this issue.
I discovered that I was not sending the API scope to the OAuth token endpoint. To do this I needed to change the input for acquireTokenSilent.
The updated code flow to login user and request data from the API:
import {publicClientApplication} from "../../components/Auth/Microsoft";//a preconfigured instance of PublicClientApplication from #azure/msal-browser
const data = await publicClientApplication.loginPopup();
// ... some data validation
publicClientApplication.setActiveAccount(data.account);
// .. some time and other processes may happen here so we don't access token directly from loginPopup()
const activeAccout = publicClientApplication.getActiveAccount();
const token = publicClientApplication.acquireTokenSilent({scopes:["api://XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/.default"],account:activeAccount}).accessToken;//here scopes is an array of strings, Where I used the api URI , but you could directly use a scope name like User.Read if you had it configured
const endpointData = await api()/*an instance of Axios.create() with some pre-configuration*/.get(
'/endpoint',
{ headers: {'Authorization': `bearer ${token}`} }); // returns status 401

Related

csurf with React: Invalid token after changing user

I've had csrf protection with the csurf module working for a while now on my React SPA. I am also using passport for authentication. I do not do any server-side rendering, so the server sends a csrf token in the response body to the client when it hits the /users/current endpoint, which is protected with csrfProtection, something like this:
import csrf from 'csurf';
const csrfProtection = csrf();
router.get("users/current", csrfProtection, async function(req, res)
{
.....
res.write(JSON.stringify({ ..., csrfToken: req.csrfToken() }));
res.end();
}
On the client side I then add the token to all subsequent request headers, a bit like this:
axiosInstance.get("/users/current")
.then(resJson =>
{
axiosInstance.interceptors.request.use(config =>
{
config.headers["x-csrf-token"] = resJson.data.csrfToken;
return config;
});
}
My first question is how the first request even manages to pass the csrfProtection without a token in its header. Yet since the token can only be accessed on the server to send to the client if the route is csrf protected, I don't see a way around this, and it does work somehow.
However, recently I have been getting "ForbiddenError: invalid csrf token" when a user logs in or deletes their account. This has only started happening after I upgraded all my node packages to the latest versions. First the client makes a request to /users/login to submit the username & password, and then makes a request to /users/current to get the new csrf token:
axiosInstance.post("/users/login", {
"username": login.username,
"password": login.password
})
.then(async resJson =>
{
// *code to update user details in redux store*
// ......
axiosInstance.interceptors.request.use(config =>
{
config.headers["x-csrf-token"] = undefined;
return config;
});
return resJson;
})
.then(async resJson =>
{
const { csrfToken } = await axiosInstance.get("/users/current")
.then(resJson => resJson.data);
axiosInstance.interceptors.request.use(config =>
{
config.headers["x-csrf-token"] = csrfToken;
return config;
});
return resJson.data;
}
I suspect it's something to do with subsequent requests coming from a different userId (which I obtain from req.user[0].userId), with which csurf will not accept the previously issued token. But I have no idea how to issue the new token csurf does expect, to the client. And it still doesn't explain why what I had before has suddenly stopped working since none of my logic has changed. This isn't the kind of error I'd typically expect after package updates.
Here someone mentions you can just set any header on the client and have the server check for that. atm I am adding the csrf token to all the client's request headers and using the csurf module's request handler function to check it, but there is nothing stopping me from writing my own. If this is true, the value of the header doesn't even matter, just that it exists. I am holding off on this option though because I feel there is something basic I'm not understanding about my current setup, which once rectified will mean this can be easily fixed.
Would appreciate any help or explanation! Thanks 🤍

Within a React app, what is the correct way to query an API that requires an Oauth token

Ebay is closing its Finding API and my React application needs to move to the Browse API https://developer.ebay.com/api-docs/buy/browse/overview.html and I need to use Oauth. My app does not require a user to authenticate with Ebay, it just shows the ebay auctions that are relevant to the page they are looking at. Within my Ebay developer account I have an application that provides an App ID (Client ID) and Cert ID (Client Secret), however I just cannot figure out how to manage the refresh token request and the application token request so that I can actually send a request to the Browse API.
My question is: within a React application, what is the correct way to use oauth refresh and authorisation tokens, and use them to successfully make a request to an Oauth protected endpoint (in this case Ebay’s)?
I have an Ebay React component (shown at the bottom of this question) and I’ve ‘borrowed’ much of this from How to get access_token using refresh token from Ebay?
However in that question const refreshToken = 'xxxxxxxx'; is mentioned and I can’t figure out how to get one. Ebay has documentation on using a refresh token https://developer.ebay.com/api-docs/static/oauth-refresh-token-request.html which says:
Configuring the request payload
Format the payload of your POST request with the following values:
- Set grant_type to refresh_token.
- Set refresh_token to the refresh token value returned from the authorization code grant request.
So this looks as though I get the refresh token in a response to an authorisation request which seems to be documented at https://developer.ebay.com/api-docs/static/oauth-client-credentials-grant.html for applications such as mine (not users). Following this example I can make a cURL request
curl -X POST 'https://api.ebay.com/identity/v1/oauth2/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Authorization: Basic U3DFSDF888888D00000000000000=' \ //not the real details
-d 'grant_type=client_credentials&scope=https%3A%2F% 2Fapi.ebay.com%2Foauth%2Fapi_scope'
-u 'StuartBrr-PRD-cd00000-0000000:PRD-0000-00000-0000-000-000' //not the real details
Which returns
{"access_token":"v^1.1#i^1#p^1#I^3#r^0#f^0#t^H4sI……","expires_in":7200,"token_type":"Application Access Token"}%
But there is no mention of a refresh token. So I suppose I could run a request against the API using that token, but it would only work until the refresh token expired.
If I run
curl 'https://api.ebay.com/identity/v1/oauth2/token'
-d 'grant_type=refresh_token&scope=https%3A%2F% 2Fapi.ebay.com%2Foauth%2Fapi_scope'
-u 'StuartBrr-PRD-cd00000-0000000:PRD-0000-00000-0000-000-000'
I get a response {"error":"invalid_grant","error_description":"the provided authorization refresh token is invalid or was issued to another client"}%
If I run my React application including the below component I can see a preflight request which is successful, and another request which reports a CORS error.
The request with the CORS error is passing the below headers in the payload
So I can’t even get the response which only gives me the Application Access Token, let alone make a subsequent call to the Ebay Browse API
Any guidance very much appreciated!
Below if the React component I have that attempts to get the token and I would like to make the API request
import React from "react";
import axios from "axios";
import queryString from 'query-string';
class Ebay extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
results: true,
error: "",
data: [],
};
}
componentDidMount() {
//Prod
const appID = 'StuartBrr-PRD-cd00000-0000000; // these are obviously not the real details
const certID = 'PRD-0000-00000-0000-000-000;
const URL = 'https://api.ebay.com/identity/v1/oauth2/token';
//sandbox
// const appID = 'StuartBr-STr-SBX-b0000000-00000000';
// const certID = 'SBX-0000000-00000-000-0000-0000;
// const URL = 'https://api.sandbox.ebay.com/identity/v1/oauth2/token';
const token = Buffer.from(`${appID}:${certID}`);
const scopes = [
'https://api.ebay.com/oauth/api_scope'
]
const { data } = axios.post(URL, {
'headers': {
'Authorization': `Basic ${token.toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
})
console.log("HERE IS AXOIS DATA "+ JSON.stringify(data));
}
render() {
console.log("IN EBAY COMPONENT");
// Render results in a table
}
}
export default Ebay;
You have to setup auth_accepted_url in developer account first,
then call
https://auth.ebay.com/oauth2/authorize with all parameters
in this way, after authentication, in auth_accepted_url
you will receive, together with auth_token, the refresh_token with its lifespan.
eBay documentation is not very clear so it requires you to read it twice.

Blocked by CORS policy "...does not have HTTP ok status" (Amplify and ReactJS, AWS Gateway and Lambda)

I'm almost embarassed to be asking this question due to CORS support out there on SO but I can't get by:
Access to XMLHttpRequest at 'https://a93xxxxx.execute-api.eu-west-1.amazonaws.com/dev[object%20Object]' from origin 'https://www.example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
I've even published my React project with Amplify and attempted it from the real domain name to even eliminate anything to do with the development environment (Cloud 9 running npm version 6.14.8)
I've also made a test running Chrome with the --disable-web-security flag.
My Lambda function contains the following (out of the box stub)
exports.handler = async (event) => {
// TODO implement
const response = {
statusCode: 200,
// Uncomment below to enable CORS requests
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers" : "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With",
"Access-Control-Allow-Methods" : "OPTIONS,POST,GET,PUT"
}
,
body: JSON.stringify("Hello from Lambda!")
};
return response;
};
Note that I've uncommented the CORS request part and the response statusCode is set to 200.
The code in my application that execute when a submission form is sent from the client:
uploadcontactusdata = async data => {
try {
console.log("Contact Us pressed")
const settings = {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
}
}
const fetchResponse = await API.post('econtactus', settings);
Notification({
title: 'Success',
message: 'Notification has been sent',
type: 'success'
});
}
catch (err) {
console.log("unable to send");
console.error(err)
}
}
I created the API Gateway + Lambda using Amplify (version 4.41.2). Not sure where else to look now. Any clues will be appreciated. Thanks
You can completely get past the need for api gateway by using appsync.
amplify add api
Choose graphql (I have not tried using rest but you shouldn't need it) choose the basic schema, edit it if you'd like, and publish. Once it's published you can create your own method. You can view this inside the AppSync UI under Schema.
type Mutation {
yourMethod(input: Input!): TableName <-- add your method to the list
}
Now inside Appsync choose Data Sources and add datasource. Give it a name, choose lambda as the type, then find your lambda in the list. Once it's added go back to your schema and find the method you created above. On the right side bar locate your method and click the attach link. Find the data source you just added. Fill out the region and lambda ARN. MAKE SURE you choose new role and not an existing one.
You might need to configure the request and response templates.
For request:
{
"version" : "2017-02-28",
"operation": "Invoke",
"payload": $util.toJson($context.args)
}
For response:
$util.toJson($context.result)
Now you can call your lambda directly from the UI and return your result without worrying about CORS or managing API Gateway.

How do I manage an access token, when storing in local storage is not an option?

I have a ReactJS app running in browser, which needs access to my backend laravel-passport API server. So, I am in control of all code on both client and server side, and can change it as I please.
In my react app, the user logs in with their username and password, and if this is successful, the app recieves a personal access token which grants access to the users data. If I store this token in local storage, the app can now access this users data by appending the token to outgoing requests.
But I do not want to save the access token in local storage, since this is not secure. How do I do this?
Here is what I have tried:
In the laravel passport documentation, there is a guide on how to automatically store the access token in a cookie. I believe this requires the app to be on the same origin, but I cannot get this to work. When testing locally, I run the app on localhost:4000, but the API is run on my-app.localhost. Could this be a reason why laravel passport does not make a cookie with the token, although they technically both have origin localhost?
OAuth has a page on where to store tokens. I tried the three options for "If backend is present", but they seem to focus on how the authorization flow rather than how to specifically store the token.
Here's the relevant parts of my code (of course, feel free to ask for more if needed):
From my react app:
const tokenData = await axios.post(this.props.backendUrl + '/api/loginToken', { email: 'myEmail', password: 'myPassword' })
console.log('token data: ', tokenData)
const personalAccessToken = tokenData.data.success.token;
var config = {
headers: {
'Authorization': "Bearer " + personalAccessToken
};
const user = await axios.get(this.props.backendUrl + '/api/user', config);
From the controller class ApiController:
public function loginToken()
{
if (Auth::attempt(['email' => request('email'), 'password' => request('password')])) {
$user = Auth::user();
$success['token'] = $user->createToken('MyApp')->accessToken;
return response()->json(['success' => $success], 200);
} else {
return response()->json(['error' => 'Unauthorised'], 401);
}
}
and the loginToken function is called from the /api/loginToken route.
Expected and actual results:
Ideally, I would love to have the token saved in a cookie like in the passport documentation, so I don't even have to attach the token to outgoing requests from the react app, but I'm not sure that this is even possible. Perhaps with third party cookies?
Else, I'd just like to find some way to store the token securely (for example in a cookie?), and then append it to outgoing calls from the react app.

How To Setup Minimalist Authentication In Rails with React?

I am trying to set up a minimal layer of authentication between my Rails backend and my React front end, but I am running into some problems.
I cannot seem to find the cookie key value that the server passes down to my client. In the network tab, I see it in the response: Set-Cookie:_skillcoop_session=...., but when I use js-cookie to look for the above cookie, _skillcoop_session, I only see one called identity-token=... and its value is different from _skillcoop_session. How do I access _skillcoop_session in the browser?
What header key do I pass up to the server to signal to my backend to use 'this' header key to match up with the session it has stored off? In this post, Justin Weiss seems to suggest that I make the request to the server with a header like: Cookie: _skillcoop_session=....
Am I doing this all wrong? Would I be better off using a gem like devise?
Also in order to load the session in my other controllers, I have had to do something like session['init'] = true, and I learned to do this from this SO post. This seems hacky. Why do I have to manually reload the session in separate controller actions after I've set it previously in a different controller action in a different request?
I'm currently just stubbing out the user and the authentication -- all I want to do to get the plumping in place is set a session[:user_id] and be able to read that session data in other controller actions. For this I have two main files for consideration: UsersController and Transport.js. In UsersController I am just stubbing the session[:user_id] with the number 1 and in Transport.js I'd like to pass the cookie received from the server so that the backend can maintain a session between requests with a client.
Here is my controller:
class UsersController < ApplicationController
def create
session[:user_id] = 1
render json: user_stub, status: :ok
end
def show
puts "user id: #{session[:user_id]}"
# should return, 1, but is returning, nil...why?
render json: user_stub, status: :ok
end
private
def user_stub
{
id: 1,
email: params['email'] || 'fakeemail#gmail.com',
password: params['password'] || 'fake password'
}
end
end
Here is the main location of my app where I make my request to the server - it's in an abstraction I call Transport.js:
require('es6-promise').polyfill();
require('isomorphic-fetch');
var cookie = require('js-cookie');
const GET = 'GET';
const POST = 'POST';
function Transport() {
}
Transport.prototype.get = function(url, options = {}) {
return this.query(GET, url, null, options);
};
Transport.prototype.post = function(url, dataString, options = {}) {
return this.query(POST, url, dataString, options);
};
Transport.prototype.query = function(method, url, dataString, options = {}) {
var data;
if (dataString) {
data = JSON.parse(dataString);
}
switch(method) {
case GET:
return fetch(url, Object.assign({headers: {'Cookie': cookie.get('_skillcoop_session')}}, options, {
method: method
}));
case POST:
return fetch(url, Object.assign({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}, options, {
method: method
}));
default:
throw new Error("This HTTP Method is not supported.");
}
};
module.exports = Transport;
According to this SO post, one cannot access the Set-Cookie header in JS. Thus, I suppose my attempts to handle Set-Cookie in the response headers was a fools effort.
According to the NPM package that I'm using to make HTTP requests, I need to pass {credentials: 'same-origin'} key value pair in the second argument to fetch, which will 'automatically send cookies for the current domain'. That did the trick -- the session object is available and contains the user_id that was set in the session in the previous request in a different action.
Yes. I changed up how I approached this problem. I leaned very heavily on this Reddit post. In short, I use ruby-jwt on the backend and store the token in localStorage on the front end. Each request out to the server will include the token in a header AUTHORIZATION.
In following steps 1 and 2, it looks like I no longer have to 'reload the session'.

Resources