I am having trouble with testing my oauth-secured application.
The problem manifests itself when there is no public page - user is immediately redirected to OAuth server it they are not authenticated.
I managed to reproduce the problem in much simpler setup:
fake app running in fake-app domain
fake oauth server running in fake-oauth-server domain
Here are respective apps (in Flask):
Fake app
from flask import Flask, redirect, render_template_string
app = Flask(__name__)
app_host="fake-app"
app_port=5000
app_uri=f"http://{app_host}:{app_port}"
oauth_host="fake-oauth-server"
oauth_port=5001
oauth_uri=f"http://{oauth_host}:{oauth_port}"
#app.route('/')
def hello():
return render_template_string('''<!doctype html>
<html>
<body>
<p>Hello, World MainApp!</p>
<a id="loginButton" href="{{ oauth_uri }}?redirect_uri={{ app_uri }}">Login</a>
</body>
</html>
''',
oauth_uri=oauth_uri,
app_uri=app_uri
)
#app.route('/goto-oauth')
def goto_oauth():
return redirect(f"{oauth_uri}?redirect_uri={app_uri}")
if __name__ == '__main__':
app.run(host=app_host, port=app_port)
Fake oauth server:
from flask import Flask, render_template_string, request
app = Flask(__name__)
oauth_host="fake-oauth-server"
oauth_port=5001
#app.route('/')
def login():
return render_template_string(
'''<!doctype html>
<html>
<body>
<p>Please log in</p>
<label>Username: <label><input id="username" />
<label>Password: <label><input id="password" />
<a id="submit-password" href="{{ redirect_uri }}">Submit</a>
</body>
</html>
''', redirect_uri=request.args.get('redirect_uri'))
if __name__ == '__main__':
app.run(host=oauth_host, port=oauth_port)
First flow: there is a publicly available page with Login button
This is possible to test with cy.origin:
describe('My Scenarios', () => {
beforeEach(() => {
cy.visit('/');
cy.contains('MainApp');
cy.get('a#loginButton').click();
cy.origin('http://fake-oauth-server:5001', () => {
cy.contains('Please log in');
cy.get('input#username').type('user1');
cy.get('input#password').type('password1');
cy.get('a#submit-password').click()
});
});
it.only('test flask', () => {
cy.visit('/');
cy.contains('MainApp');
});
});
Problematic flow: immediate redirect to Oauth server
describe('My Scenarios', () => {
beforeEach(() => {
cy.visit('/goto-oauth');
cy.origin('http://fake-oauth-server:5001', () => {
cy.contains('Please log in');
cy.get('input#username').type('user1');
cy.get('input#password').type('password1');
cy.get('a#submit-password').click()
});
});
it.only('test flask', () => {
cy.visit('/');
cy.contains('MainApp');
});
});
Fails with:
CypressError: `cy.origin()` requires the first argument to be a different domain than top. You passed `http://fake-oauth-server:5001` to the origin command, while top is at `http://fake-oauth-server:5001`.
Either the intended page was not visited prior to running the cy.origin block or the cy.origin block may not be needed at all.
There is no publicly available page in my app - how can I amend the test to make it work?
It seems to work if visit the redirect URL inside the cy.origin().
I set the app on http://localhost:6001 and the auth server on http://localhost:6003, using express rather than flask.
Test
describe('My Scenarios', () => {
beforeEach(() => {
cy.origin('http://localhost:6003', () => {
cy.visit('http://localhost:6001/goto-oauth')
cy.contains('Please log in');
cy.get('input#username').type('user1');
cy.get('input#password').type('password1');
cy.get('a#submit-password').click()
});
});
it('test main app', () => {
cy.visit('http://localhost:6001')
cy.contains('MainApp')
})
})
App
const express = require('express')
function makeApp() {
const app = express()
app.get('/', function (req, res) {
res.send(`
<html>
<body>
<p>Hello, World MainApp!</p>
<a id="loginButton" href="http://localhost:6003?redirect_uri=http://localhost:6001">
Login
</a>
</body>
</html>
`)
})
app.get('/goto-oauth', function (req, res) {
res.redirect('http://localhost:6003')
})
const port = 6001
return new Promise((resolve) => {
const server = app.listen(port, function () {
const port = server.address().port
console.log('Example app listening at port %d', port)
// close the server
const close = () => {
return new Promise((resolve) => {
console.log('closing server')
server.close(resolve)
})
}
resolve({ server, port, close })
})
})
}
module.exports = makeApp
Auth
const express = require('express')
function makeServer() {
const app = express()
app.get('/', function (req, res) {
res.send(`
<!doctype html>
<html>
<body>
<p>Please log in</p>
<label>Username: <label><input id="username" />
<label>Password: <label><input id="password" />
<a id="submit-password" href="http://localhost:6001">Submit</a>
</body>
</html>
`)
})
const port = 6003
return new Promise((resolve) => {
const server = app.listen(port, function () {
const port = server.address().port
console.log('Example app listening at port %d', port)
// close the server
const close = () => {
return new Promise((resolve) => {
console.log('closing server')
server.close(resolve)
})
}
resolve({ server, port, close })
})
})
}
module.exports = makeServer
Related
I'm trying to authenticate to Xero's API. I get a 'code' which is then exchanged for an access_token. I'm still new to NextJS and React so I'm likely not thinking about this correctly.
The code I have results in the right data being returned, however I don't know how to use it effectively in the rest of the app. I couldn't figure out how to use NextAuth in a custom provider so tried to roll my own.
The user clicks the button 'Connect to Xero' - this is a href to initiate the process and takes the user to Xero to login in the browser. User authenticates. Xero calls the callback
the callback at /api/callback responds
I extract the 'code' and then make the subsequent request to Xero to swap it for an access token.
This is where I get stuck - because the initial action is a href redirect, I'm not sure how to get the end API result back into my code as state/something usable. In effect Xero is calling the api/callback page and that's where the user is left.
I've tried to put useState hooks into the api/callback however that breaks the rule of hooks.
Any pointers greatly appreciated.
Code;
pages/index.js
import React from 'react'
import Layout from '../components/Layout'
import TopNav from '../components/TopNav'
import Link from 'next/link';
export default function Main(props) {
const test = props.URL
return (
<>
<Layout>
<TopNav name="Main page"/>
<p>this is the main page</p>
<Link href={test} passHref={true}>
<button className=' w-40 border rounded-md py-3 px-3 flex items-center justify-center text-sm font-medium sm:flex-1'>
Connect to Xero
</button>
</Link>
</Layout>
</>
)
}
export async function getStaticProps() {
const XeroAuthURL = "https://login.xero.com/identity/connect/authorize?response_type=code&client_id="
const client_ID = process.env.XERO_CLIENT_ID
const redirect_uri = process.env.XERO_REDIRECT_URI
const scope = "offline_access openid profile email accounting.settings"
const URL = `${XeroAuthURL}${client_ID}&redirect_uri=${redirect_uri}&scope=${scope}`
return {
props: {
URL: URL
},
};
}
/api/callback.js
import axios from "axios"
const qs = require('qs');
export default async function callback(req, res) {
try {
//callback from Xero will deliver the code, scope + state (if given)
//https://developer.xero.com/documentation/guides/oauth2/auth-flow/#2-users-are-redirected-back-to-you-with-a-code
console.log(`REQ = ${JSON.stringify(req.query)}`)
//exchange code for tokens - https://developer.xero.com/documentation/guides/oauth2/auth-flow/#3-exchange-the-code
var data = qs.stringify({
'code': req.query.code,
'grant_type': 'authorization_code',
'redirect_uri': 'http://localhost:3000/api/callback'
});
var config = {
method: 'post',
url: 'https://identity.xero.com/connect/token',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic **put your authorisation result here**'
},
data : data
};
try {
const response = await axios(config)
//response has the data I want to put into State
console.log(JSON.stringify(response.data));
//save data off here somehow???
//tried redirecting but unsure if can pass the result
res.redirect(307, '/')
} catch (error) {
console.error(error)
res.status(error.status || 500).end(error.message)
}
} catch (error) {
console.error(error)
res.status(error.status || 500).end(error.message)
}
}
Added a not-secure cookie that I can use while testing. Do not use this in production as the cookie is not httpOnly and not secure.
import axios from "axios"
import Cookies from 'cookies'
const qs = require('qs');
export default async function callback(req, res) {
const cookies = new Cookies(req,res)
try {
var data = qs.stringify({
'code': req.query.code,
'grant_type': 'authorization_code',
'redirect_uri': 'http://localhost:3000/api/callback'
});
var config = {
method: 'post',
url: 'https://identity.xero.com/connect/token',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic **YOUR AUTH CODE HERE**'
},
data : data
};
try {
var response = await axios(config)
response.data.expires_at = Date.now() + response.data.expires_in*1000
//save data off
//TO DO - THIS IS REALLY BAD - ONLY USE THIS TEMPORARILY UNTIL HAVE GOT PERMSTORAGE SETUP
cookies.set('myCookieName', JSON.stringify(response.data), {
secure: false,
httpOnly: false
}
)
res.redirect(307, '/')
//return ({ data: response.data })
} catch (error) {
console.error(error)
res.status(error.status || 500).end(error.message)
}
} catch (error) {
console.error(error)
res.status(error.status || 500).end(error.message)
}
}
Then in the index;
import React from 'react'
import Layout from '../components/Layout'
import TopNav from '../components/TopNav'
import Link from 'next/link';
import { getCookie } from 'cookies-next';
export default function Main(props) {
//check for cookie
//TO DO THIS IS REALLY BAD; CHANGE WHEN GET PERM STORAGE ORGANISED
const cookie = getCookie('myCookieName');
const URL = props.URL
return (
<>
<Layout>
<TopNav name="Main page"/>
<p>this is the main page</p>
<Link href={URL} passHref={true}>
<button className=' w-40 border rounded-md py-3 px-3 flex items-center justify-center text-sm font-medium sm:flex-1'>
Connect to Xero
</button>
</Link>
<p>{cookie ? cookie : 'waiting for cookie...'}</p>
</Layout>
</>
)
}
export async function getStaticProps() {
const XeroAuthURL = "https://login.xero.com/identity/connect/authorize?response_type=code&client_id="
const client_ID = process.env.XERO_CLIENT_ID
const redirect_uri = process.env.XERO_REDIRECT_URI
const scope = "offline_access openid profile email accounting.settings"
//console.log(`URL - ${XeroAuthURL}${client_ID}&redirect_uri=${redirect_uri}&scope=${scope}`)
const URL = `${XeroAuthURL}${client_ID}&redirect_uri=${redirect_uri}&scope=${scope}`
return {
props: {
URL: URL,
},
};
}
I am using laravel passport for authentication in my laravel and vue.js ecommerce project.
After successful login, I want to redirect user to his/her dashboard.
Here is the vue dashboard page:
<template>
<div class="content-center">
<h1>Dashboard my account</h1>
<p>
{{userData.name}}
</p>
</div>
</template>
<script>
import Axios from "axios";
export default {
data() {
return {
userData: "",
authToken: ""
}
},
async beforeMount() {
let res = await Axios.get("http://localhost:8000/api/user-details", {
headers: {
Authorization: "Bearer " + this.authToken,
Accept: 'application/json'
},
});
this.userData = res.data;
// let token = await Axios.get("http://localhost:8000/api/user-login")
// this.authToken = res.data.data.auth_token
//let res = await Axios.get("http://localhost:8000/api/user-details");
},
};
</script>
Everytime I login to different user accounts, I have to set the value of authToken manually copy and pasting from Postman. I want to set this token automatically when a user logs in. How can I do this ?
Here is my api controller:
class AuthApiController extends Controller
{
public function userDetails(){
return auth()->user();
}
public function login(Request $request){
$user = User::where('email',$request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json([
'success'=>false,
'data'=>[],
'message'=>'Login failed',
'errors'=>[]
]);
}else{
return response()->json([
'success'=>true,
'data'=>['user'=> $user, 'auth_token' => $user->createToken('AuthToken')->accessToken],
'message'=>'Login success',
'errors'=>[]
]);
}
}
Updates:
dashboard.vue
<template>
<div class="content-center">
<h1>Dashboard my account</h1>
<p>
{{userData.name}}
</p>
</div>
</template>
<script>
import Axios from "axios";
export default {
data() {
return {
userData: "",
authToken: ""
}
},
async beforeMount() {
let res = await Axios.get("http://localhost:8000/api/user-details", {
headers: {
Authorization: "Bearer " + this.authToken,
Accept: 'application/json'
},
});
this.userData = res.data;
let token = await $api.get("http://localhost:8000/api/user-login")
this.authToken = res.data.data.auth_token
},
};
</script>
Picture:
enter image description here
What should I write to import api.js ?
import $api from ./../api.js or anything else ?
Well, you can store your token in LocalStorage. And whenever you request just get it from the local storage and pass it to the request header.
If you are using Axios then you can use interceptors and just intercept your every request and pass token in the header.
Step 1.
Create a file called api.js or you can call it whatever you want.
Step 2.
Create an Axios instance in the api.js file.
import axios from 'axios';
// Put your backend url here
export const API_URL = `http://localhost:5000/api`
const $api = axios.create({
withCredentials: true,
baseURL: API_URL
})
$api.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`
return config;
})
export default $api;
Step 3: Where ever you are using Axios use this exported instance so in your component you would do like this:
const userdata = await $api.get("http://localhost:8000/api/user-details");
Here you can see, we are using the $api Axios instance which we created in the api.js file instead of Axios direct.
Add also don't forget to store your token in your local storage when you getting that.
localStorage.setItem('token', "Your token goes here...");
I hope this will give you an idea.
This way, Token will be sent with every request automatically, if it exists in the localStorage.
UPDATE:
<template>
<div class="content-center">
<h1>Dashboard my account</h1>
<p>
{{userData.name}}
</p>
</div>
</template>
<script>
// import Axios from "axios";
import $api from 'put relative path of your api.js file'
export default {
data() {
return {
userData: "",
authToken: ""
}
},
async beforeMount() {
let res = await $api.get("/user-details");
this.userData = res.data;
let res = await $api.get("/user-login")
localStorage.setItem('token', res.data.data.auth_token);
},
};
</script>
I am now using the newest version of Alpine which is v3.
Making reusable components needs to be registered using the Alpine.data.
This is the alpinejs.js
import Alpine from 'alpinejs'
import form from './components/form'
window.Alpine = Alpine
Alpine.data('form', form)
Alpine.start()
This is what I have in the components/form.js
export default (config) => {
return {
open: false,
init() {
console.log(config)
},
get isOpen() { return this.open },
close() { this.open = false },
open() { this.open = true },
}
}
This is the html part:
<div x-data="form({test:'test'})"></div>
This is the error I get in the console:
Any idea how to pass parameters to Alpine.data?
I stumbled over this question, searching for an answer but figured it out now. Maybe its still usefull to someone...
You have do define the parameter when registering the data component:
document.addEventListener('alpine:init', () => {
window.Alpine.data('myThing', (param) => MyModule(param));
});
Now you can use it in your module on init...
export default (param) => ({
init() {
console.log(param);
}
});
... when you init the component
<div x-data="deliveryDate({ foo: 'bar' })"></div>
This likely happens since you imported your script as a module. Therefore, you need another script that handles initialization of data.
I'm using a vanillajs vite setup and here's a working implementation with alpinejs:
index.html
<head>
<!-- Notice the type="module" part -->
<script type="module" src="/main.js" defer></script>
<script src="/initializer.js"></script>
</head>
<body x-data="greetingState">
<button #click="changeText">
<span x-text="message"></span>
</button>
<h2 x-text="globalNumber"></h2>
</body>
main.js
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
// const globalNumber = 10; // Wrong place
initialize.js
document.addEventListener('alpine:init', () => {
Alpine.data('greetingState', () => ({
message: "Hello World!",
changeText() {
this.message = "Hello AlpineJs!";
},
}));
});
const globalNumber = 10; // Correct place
Note that listening to the alpine:init custom event inside of a javascript module will break the app. The same happens if you try to display a variable from a script of type module, in this example globalNumber.
I have a main page containing a component called single-contact as below:
<el-row id="rowContact">
<!-- Contacts -->
<el-scrollbar wrap-class="list" :native="false">
<single-contact ref="singleContact"></single-contact>
</el-scrollbar>
</el-row>
And I want to dynamically render this component after AJAX polling, so in SingleContact.vue I use $axios and mounted() to request the data from the backend. And I want to render the component using v-for. I have my code as:
<template>
<div :key="componentKey">
<el-row id="card" v-for="contact in contacts" :key="contact.convUsername">
<div id="avatar" ><el-avatar icon="el-icon-user-solid"></el-avatar></div>
<h5 id='name' v-if="contact">{{contact.convUsername}}</h5>
<div id='btnDel'><el-button size="medium" type="danger" icon="el-icon-delete" v-on:click="delContact(contact.convUsername)"></el-button></div>
</el-row>
</div>
</template>
And the data structure is:
data() {
return {
timer: null,
contacts: []
}
And the method of Ajax polling is:
loadContacts () {
var _this = this
console.log('loading contacts')
console.log(localStorage.getItem('username'))
this.$axios.post('/msglist',{
ownerUsername: localStorage.getItem('username')
}).then(resp => {
console.log('success')
var json = JSON.stringify(resp.data);
_this.contacts = JSON.parse(json);
console.log(json);
console.log(_this.contacts[0].convUserName);
// }
}).catch(failResponse => {
console.log(failResponse)
})
}
This is what I get in the console:
Console Result
And the mounted method I compute is as:
beforeMount() {
var self = this
this.$axios.post('/msglist',{
ownerUsername: localStorage.getItem('username')
}).then(resp => {
this.$nextTick(() => {
self.contacts = resp.data
})
}).catch(failResponse => {
console.log(failResponse)
})
},
mounted() {
this.timer = setInterval(this.loadContacts(), 1000)
this.$nextTick(function () {
this.loadContacts()
})
},
beforeDestroy() {
clearInterval(this.timer)
this.timer = null
}
I can get the correct data in the console. It seems that the backend can correctly send json to the front, and the front can also receive the right result. But the page just doesn't render as expected.
Any advice would be great! Thank you in advance!
I am trying to submit a simple form in a React component:
class UploadPartList extends Component {
constructor(props) {
super(props);
this.state = { data: [] };
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
console.log('Clicking submit');
$.ajax({
type: "POST",
url: "/partListUpload",
success: function(){
console.log("Post success");
},
error: function(xhr, status, error) {
console.log(error);
}
})
}
render() {
return (
<div>
<form id="csvForm" action='' onSubmit={this.handleSubmit} method='post' encType="multipart/form-data">
<p>upload your part number list (.xls or .csv)</p>
<input id="uploadCSV" type="file" name="csv_form" />
<input type="submit" className="submitButton" />
</form>
</div>
);
}
}
My routes.js file is thus:
import React from 'react';
import { Router, Route } from 'react-router';
import App from './components/App';
const Routes = (props) => (
<Router {...props}>
<Route path="/" component={App} />
</Router>
);
export default Routes;
And my Express routes in the server are defined like this:
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, '..', 'build', 'index.html'));
});
app.use('/partListUpload', upload.single('csv_form'), partListUploadController);
app.post('/partListUpload', function(req, res) {
res.send(req.body);
});
However, when I try to submit the form, I receive a 404 error. It seems that React expects the route defined by React Router instead of a route I define in my server.
I have looked over similar StackOverflow questions and haven't found a solution that works. How can I hit a route I define on the backend?
It seems that my problem was that my Express server was not actually running. I created this app using the create react app package. I then followed this guide in setting it up with Express. However, I continued trying to get my app to work by just running npm start in the CLI instead of npm run build and then node server/index.js. Now it works.