React + Laravel + Sanctum for api token authentication(NOT cookie) - php

I am trying to get React and Laravel to work together using the middleware Sanctum.
I can read many examples of people trying to do this with a cookie based setup, but I am trying to use the token setup for a pure API approach. I am doing this because I want to prepare the backend for use in a mobile app where cookies are not available.
This is a part of my setup:
/backend/routes/api.php:
Route::post('/login', [ UserController::class, 'getAccessToken'] );
/frontend/store/api.js
static login( user ) {
var formData = new FormData();
formData.append('username', user.username);
formData.append('password', user.password )
formData.append('deviceName', 'browser');
return fetch(
'http://localhost:5001/api/login, {
method : 'post',
body : formData
}
);
}
My problems is that it forces a CSRF token check, when the login route is accessed.
That is even if the login route shouldn't be guarded by Sanctum.
This of course fails when I am logging in and don't yet have a token to attach to the request.
As I understand the token is only needed on accessing guarded routes after login.
I have double checked that it is accessing the right route by renaming it to something fake and getting an error.
Am I doing something wrong with the use of Sanctum or is Sanctum just not the preferred use for api tokens? Should I maybe look into JWT instead?
Thank you in advance for your help. <3

Please, check this url, I was able to make it work thanks to this tutorial.
https://laravel-news.com/using-sanctum-to-authenticate-a-react-spa
here is my LoginForm.jsx
import React from "react";
import apiClient from "./services/apiClient";
const LoginForm = (props) => {
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const handleSubmit = (e) => {
e.preventDefault();
apiClient.get("/sanctum/csrf-cookie").then((response) => {
apiClient
.post("/api/sanctum-token", {
email: email,
password: password,
device_name: "React v0.1",
})
.then((response) => {
console.log(response);
});
});
};
return (
<div>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
name="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">Login</button>
</form>
</div>
);
};
export default LoginForm;
apiClient.js
import axios from "axios";
const apiClient = axios.create({
baseURL: "http://localhost",
withCredentials: true,
});
export default apiClient;

In:
/backend/app/Http/Kernel.php
I had added:
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
It worked when I removed that line. I had failed to understand that a SPA equals the use of cookies, because I would say that I am also working on a SPA that just uses API tokens instead.
Next issue is that I am using Doctrine instead of Eloquent, which I can now see it far from compatible with Sanctum when it comes to issuing the token. But that will be a topic for another question.

I use this tutorial https://medium.com/#suton.tamimy/basic-token-authentication-with-fresh-laravel-8-sanctum-and-reactjs-ef14eba7ce0f
default setting laravel (no setting modifications etc) with API Login output TOKEN and BEARER token type. Then in ReactJs, just use this axios setting to generate the token (no need sanctum/csrf-cookie).
axios.defaults.baseURL = '_MY_API_';
axios.post('/login', {
email: '_EMAIL_INPUT_FORM _',
password: '_PASSWORD_INPUT_FORM_'
})
.then(({ data }) => {
if(data.status==="success"){
console.log(data)
} else {
console.log("error")
}
});
});

Related

React application failing to fetch once publicly hosted

can anyone help me with the following issue? This is all very new to me so I'm sorry for any incoveniences. I'm creating a ChatGPT tool which takes a text inputs from the user in the front end then passes that data into the back end which sends the user input to an open OpenAI API layer. The back end then receives a response back from the OpenAI layer and stores the text response into a text array and writes it back to the front end into a standard text area.
The website runs perfectly when it is hosted locally on localhost:3001 and port:3001. My issue stems when I deploy the website to firebase and attempt to submit a form request on another machine and different network it does not writing any text to the textarea. Below I have provided my code. I believe the issue has something to do with the localhost code in the handleSubmit function or could even be in the back end script i'm very unsure and would really appreciate any help I can get thanks to get this running publicly. Thanks for your time :)
Front End (App.js)
import React, { useState } from 'react';
import './App.css';
function App() {
const [message, setMessage] = useState('');
const [response, setResponse] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
fetch('http://localhost:3001', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({message}),
})
.then((res) => res.json())
.then((data) => setResponse(data.message));
};
return (
<body>
<div className="App">
<form onSubmit={handleSubmit}>
<div class="section"></div>
<header>
<nav>
<ul class="nav_links">
<li><a href='#'>Home</a></li>
<li><a href='#'>Pricing</a></li>
<li><a href='#'>About</a></li>
<li><a href='#'>Contact</a></li>
</ul>
</nav>
<button class="login">Login</button>
</header>
<input type="text" id="topic"
value={message}
onChange={(e) => setMessage(e.target.value)}
></input>
<textarea id="textarea" value={response} />
<div> <button id="generate" type="submit">Generate</button> </div>
</form>
</div>
</body>
);
}
Back End (Index.js)
const OpenAI = require('openai');
const { Configuration, OpenAIApi } = OpenAI;
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const app = express();
const port = 3001;
const configuration = new Configuration({
organization: "org-kEBxx4hVwFZ",
apiKey: "sk-jmYuTSCZvxCjidnbTpjFT3Blbk",
});
const openai = new OpenAIApi(configuration);
app.use(bodyParser.json());
app.use(cors());
app.post('/', async (req, res) => {
const { message } = req.body;
const response = await openai.createCompletion({
model: "text-davinci-003",
prompt: `${message}`,
max_tokens: 1000,
temperature: 0,
});
console.log(response.data)
if(response.data.choices[0].text){
res.json({message: response.data.choices[0].text})
}
});
app.listen(port, () => {
console.log("Listening...")
});
Since http://localhost:3001 is hardcoded into your code, even when you deploy it in production, the production website will still try to make a request to localhost:3001. To fix this, you need to dynamically set the url based on whether the code is in development or production. The recommended way to do this is using environment variables: https://create-react-app.dev/docs/adding-custom-environment-variables/, where you'd set the env variable to localhost:3001 during development and the url of the production server in production.

Laravel Sanctum not working with Jetstream's default Login.vue

So I'm using Jetstream for the authorization part! Now I want to use Laravel sanctum's "auth:sanctum" middleware to protect my api's!
But when I call the api from my SPA, it gives me "401 (Unauthorized)" error even though I'm making csrf-cookie while logging in!
The api works fine when I call it from Postman by passing the token in Bearer token header!
My codes:
Login.vue: (This is where I'm creating the token)
axios.get('/sanctum/csrf-cookie')
.then(response => {
this.form
.transform(data => ({
... data,
remember: this.form.remember ? 'on' : ''
}))
.post(this.route('login'), {
onFinish: () => this.form.reset('password'),
})
})
AppLayout.vue: (This is where I'm calling the api)
axios.get('/api/abilities',)
.then((response)=>{
console.log(response.data);
})
.catch((error)=>{
console.log(error);
})
api.php:
Route::middleware('auth:sanctum')->get('/abilities', function (Request $request) {
return auth()->user();
});
Other things that I've already done are:
Added "axios.defaults.withCredentials = true;" to bootstrap.js
Added "SANCTUM_STATEFUL_DOMAINS=localhost:8000;" to .env file
Added "EnsureFrontendRequestsAreStateful::class" to Kernel.php

Add 2FA or OTP (via e-mail) to Laravel Fortify 2FA via Authenticator App

I am using Laravel 8.x with Fortify and Jetstream/Livewire with 2FA / OTP turned on:
config/fortify.php
'features' => [
Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Features::twoFactorAuthentication(),
],
I also customized that a bit with my route groups, etc. to allow some pages without authentication and other with. That works fine. However, many of our users are not really techies and find it to be annoying to use the Auth App (I sort of agree with that), although I like the baked in 2FA with Fortify.
What I would like to do is:
Modify the existing Fortify code to optionally allow for implementing 2FA via e-mail as well. It would probably be hard to implement one or the other, or both, so that a user could use whatever method is available, but seems like it would be possible to allow a user to pick which method they want. Built in Fortify method with an Auth App, or e-mail OTP with some other package or by modifying the Fortify code after publishing the files that are needed to modify.
I have looked around a little, and the one below seems pretty simple, but I would want to use that one for the e-mail method, and the Fortify one for the Authenticator method. That would be a user preference option.
github.com/seshac/otp-generator
I tried to do this by using the in-place Laravel 8.x JetStream Framework with Fortify by making some modifications as follows:
views/auth/login.blade.php
Changed the form to:
<form method="POST" action="{{ route('login') }}" id = "OTPlogin">
Added this after the form (I also have the Spatie Cookie reminder package).
<div id = "cookieremindertext">If not alreay done, click on "Allow Cookies". Read the Privacy Policy and Terms if you wish.</div>
<label for = "otp" id = "otpwrapper">Please Enter the OTP (One-Time Password) below and Click "LOGIN" again.
<input type="text" name="otp" id = "otp" value = ""/>
</label>
and then quite a bit of Javascript on the page as an addition:
function dynamicPostForm (path, params, target) {
method = "post";
var form = document.createElement("form");
form.setAttribute("method", method);
form.setAttribute("action", path);
if (target) {
form.setAttribute("target", "_blank");
}
for (const [key, value] of Object.entries(params)) {
if(params.hasOwnProperty(key)) {
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", key);
hiddenField.value = value;
form.appendChild(hiddenField);
}
}
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", "_token");
hiddenField.setAttribute("value", '{{ csrf_token() }}');
form.appendChild(hiddenField);
document.body.appendChild(form);
form.submit();
$(form).remove();
}
$("#OTPlogin").on("submit", function(e) {
e.preventDefault();
$.ajax({
type: "POST",
url: "/Auth/otp-generator",
dataType: "json",
data: {email:$("[name=email]").val(),password:$("[name=password]").val(),remember:$("[name=remember]").prop("checked"),otp:$("[name=otp]").val()},
context: $(this),
beforeSend: function(e) {$("body").addClass("loading");}
}).done(function(data, textStatus, jqXHR) {
if (data.error == 'otp') {
// $("#otp").val(data.otp);
$("#otpwrapper").show();
}
else if (data.message == "LOGIN") {
params = {};
params.email = $("[name=email]").val();
params.password = $("[name=password]").val();
params.remember = $("[name=remember]").prop("checked");
dynamicPostForm ('/login', params, false);
data.message = "Thank you . . . logging you in."
// This will fail on the backend if the session indicating a valid OTP did not get set.
}
showMessage("",data.message);
$(".modal-body").css("width", "320px");
});
});
That basically sort of bypasses the regular /login route initially until I get a response back from the backend indicating that the OTP has been verified via e-mail (maybe SMS later).
Added a new route in:
routes/web.php
// Receive User Name and Pass, and possibly otp from the login page.
Route::post('/Auth/otp-generator', [EmailController::class, 'sendOTP']); // move to middleware ?, Notifications or somewhere else ?
Created some new methods in my EmailController (should maybe refactor that to a different location or file, but as follows. That handles getting the OTP verified, and when it is sets: Session::put('OTP_PASS', true);
Http/Controllers/EmailController.php
protected function generateAndSendOTP($identifier) {
$otp = Otp::generate($identifier);
$expires = Otp::expiredAt($identifier)->expired_at;
try {
//$bcc = env('MAIL_BCC_EMAIL');
Mail::to($identifier)->bcc("pacs#medical.ky")->send(new OTPMail($otp->token));
echo '{"error":"otp","message":"Email Sent and / or SMS sent.<br><br>Copy your code and paste it in the field that says OTP below. Then click on the Login Button again, and you should be logged in if all is correct.", "otp":"Token Sent, expires: ' . $expires . '"}';
}
catch (\Exception $e) {
//var_dump($e);
echo '{"error":true,"message":'.json_encode($e->getMessage()) .'}';
}
}
protected function sendOTP(Request $request) {
$user = User::where('email', $request->email)->first();
// $test = (new AWS_SNS_SMS_Tranaction())->toSns("+16513130209");
if ($user && Hash::check($request->password, $user->password)) {
if (!empty($request->input('otp'))) {
$verify = Otp::validate($request->input('email'), $request->input('otp'));
// gets here if user/pass is correct
if ($verify->status == true) {
echo '{"error":false,"message":"LOGIN"}';
Session::put('OTP_PASS', true);
}
else if ($verify->status !== true) {
echo '{"error":false,"message":"OTP is incorrect, try again"}';
}
else {
// OTP is expired ?
$this->generateAndSendOTP($request->input('email'));
}
}
else if (empty($request->input('otp'))) {
$this->generateAndSendOTP($request->input('email'));
}
}
else {
echo '{"error":true,"message":"User Name / Password Incorect, try again"}';
}
}
and then finally in:
Providers/FortifyServiceProviders.php, I had a check for Session::get('OTP_PASS') === true to the validation for the login. It actually does seem to "work", but I'd like to possibly extend it to support sending the OTP via SMS if the user also has a phone number in the DB. I am using an e-mail for now because that is always populated in the DB. They may or may not have a Phone number, and I would make the notification method a user preference item.
I have AWS SNS setup on another framework, but not sure how to do that on Laravel. In the generateAndSendOTP($identifier) method, I would want to add something to check for their user preference notificaiton method, and then send the OTP to the email and / or via SMS. So that is one issue. The other issue is just the entire setup now because I probably need to move things around a bit to different locations now that is seems to be working. It would be nice to package up eventually to add OTP via e-mail and / or SMS. The baked in Authenticator App method should not really be affected, and I might want to use that to selectively protect certain routes after they log in via e-mail / SMS, for situations where possibly more than one user uses an account, like a proxy, but the account owner does not want them getting into sections protected with the built-in 2FA via Authenticator App.
Thanks.
Looking at the source code, Fortify actually uses PragmaRX\Google2FA so you can instantiate it directly.
So you can manually generate the current OTP that the authenticator app would generate and do whatever you need to with it. In your case, send an email (or SMS) during the 2FA challenge during login.
use PragmaRX\Google2FA\Google2FA;
...
$currentOTP = app(Google2FA::class)->getCurrentOtp(decrypt($user->two_factor_secret));
Although I think it was an oversight (as well as not requiring confirmation on initial key generation) that they didn't include this functionality built in.

How to fix the Vue Router guard rerouting problem?

I'm setting up a laravel, vue project and i am using JWT auth for the user authentication. I am trying to protect the Routes with Vue Router and it is getting token from the local storage and giving access to the authentic user to specific route, but once on another route if i click on any other route or refresh the page it redirects me on the "/login" page again. The token remains same all the time but it is considering the token as the user is not authentic. Please help as i am new to laravel and vue
I have tried using meta info but that didn't work as well. Moreover, i have tried deleting the token from local storage and created it again but nothing works for me.
routes.js file
export const routes = [
{
path: '/', component: Home, name: 'home'
},
{
path: '/dashboard', component: Dashboard, name: 'dashboard', meta:{requiresAuth: true}
},
{
path: '/login', component: Login, name: 'login'
}
];
App.js file
import VueRouter from 'vue-router';
import { routes } from './routes.js';
window.Vue = require('vue');
Vue.use(VueRouter);
const router = new VueRouter({
mode: 'history',
routes
});
router.beforeEach((to, from, next) => {
console.log(to)
if (to.meta.requiresAuth) {
const authUser = JSON.stringify(window.localStorage.getItem('usertoken'))
if(authUser && authUser.accessToken){
console.log("here")
next();
}else{
next({name: 'login'});
}
}
next();
});
const app = new Vue({
el: '#app',
router
});
I expect the output to be like when the user is authentic and the router.beforeEach method finds a token, the user can get to any route until the token gets deleted or changed. Moreover, the user should not be taken to '/login' everytime a <router-link> is clicked or page is refreshed.
I was just trying to solve it and it is solved...the problem was with the line if(authUser && authUser.accessToken) . I added authUser.accessToken as a condition which was not fulfilled, so it was redirecting on every click. I removed that condition and just left with if(authUser) and now it is working perfectly. Also I have added JSON.stringify to change my object to text and then authenticate with JSON.parse by passing a variable.
My final code looks like:-
router.beforeEach((to, from, next) => {
console.log(to)
if (to.meta.requiresAuth) {
var usertoken = JSON.stringify(window.localStorage.getItem('usertoken'))
const authUser = JSON.parse(usertoken)
if(authUser){
console.log("here")
next();
}else{
next({name: 'login'});
}
}
next();
});

Laravel with AngularJS Login

Im using Laravel 5 as an API and i have AngularJS running my frontend.
I have built the login portion of the backend that accepts the form data and responds with a json object.
My question is when i recieve the success object from the api to say that the login details are sucessfull. How do i use AngularJS to then login the user from the frontend.
AuthenticateUser.php
http://pastebin.com/PZqGCpz5
app.js
var app = angular.module('app', []);
app.controller('AppCtrl', function($scope, $http) {
$scope.login = {};
$scope.submitLoginForm = function () {
var email = $scope.login.email;
var password = $scope.login.password;
$http({
method : 'POST',
url : '/api/1.0/auth/login',
data : { email, password },
headers : { 'Content-Type': 'application/json' }
})
.success(function(data) {
console.log(data);
});
}
}
JSON Response
success: Object
message : authentication_successfull
code : 200
user_id : 1
What steps should i take from here to log the user into the frontend.
Thanks in advance
You can do this with the help of api_token approach.
First when you call a login api then create a unique token specific to user and save it database and send it in response as:
success: Object
message : authentication_successfull
code : 200
data : {api_token: some_random_key}
Then for subsequent request send that api_token in the request headers.
And server will automatically logins the user if you are using the auth:api middleware as:
Route::group(['middleware' => ['auth:api']], function()
{
// API routes here
});
For reference

Categories