Cypress - Status: 401 - Unauthorized in token authentication to other API call - api

I would like to ask for your help regarding the authentication token to be used in other API calls.
Below are the scripts of the command and test case:
//command.js
import "cypress-localstorage-commands";
Cypress.Commands.add('login', () => {
cy.request({
method: 'POST',
url: Cypress.env('api_auth'),
body: {
email: Cypress.env('email'),
password: Cypress.env('password'),
}
})
.its('body')
.then(body => {
cy.window().then(win => win.localStorage.setItem('jwt', body.token))
})
//test case
describe('GET Data', ()=> {
before(() => {
cy.login();
cy.saveLocalStorage();
});
beforeEach(() => {
cy.restoreLocalStorage();
});
it('GET - View All Data', () =>{
cy.request({
method : 'GET',
url : Cypress.env('api_data'),
}).then((res) =>{
expect(res.status).equal(200)
})
})

In my approach i have the same command, but instead of storing the token in the local storage, I'm adding it to the cypress.env file in the object of the admin, because i have several admins so my object file goes like so
{
admins:
admin1:{
"username":"admin1",
"password": "123456",
"token":""
},
admin2:{
"username":"admin1",
"password": "123456",
"token":""
}
}
This way you can store several admins/user tokens and just attach as:
Cypress.env.admins.admin1.token
or
Cypress.env.admins.admin2.token
I think this approach is simpler and in about 300 tests i've seen no problems regarding speed of the test execution or anything other

Related

KeystoneJS login via GraphQL mutation

I am trying to login to a Keystone 5 GraphQL API. I have setup the app so that I can login via the Admin Console, but I want to login from a Svelte application.
I keep finding references to the code below (I am new to GraphQL) but don't know how to use it.
mutation signin($identity: String, $secret: String) {
authenticate: authenticateUserWithPassword(email: $identity, password: $secret) {
item {
id
}
}
}
If I post that query "as is" I get an authentication error, so I must be hitting the correct endpoint.
If I change the code to include my username and password
mutation signin("myusername", "mypassword") {
authenticate: authenticateUserWithPassword(email: $identity, password: $secret) {
item {
id
}
}
}
I get a bad request error.
Can anyone tell me how I send username/password credentials correctly in order to log in.
The full code I am sending is this
import { onMount } from 'svelte';
let users = [];
onMount(() => {
fetch("http://localhost:4000/admin/api", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `mutation signin($identity: String, $secret: String) {
authenticate: authenticateUserWithPassword(email: $identity, password: $secret) {
item {
id
}
}
}`
})
}).then(res => res.json())
.then(data => {
console.log(data)
})
})
Here is the response I get
{"errors":[{"message":"[passwordAuth:failure] Authentication failed","locations":[{"line":2,"column":3}],"path":["authenticate"],"extensions":{"code":"INTERNAL_SERVER_ERROR","exception":{"stacktrace":["Error: [passwordAuth:failure] Authentication failed"," at ListAuthProvider._authenticateMutation (/Users/simon/development/projects/keystone/meetings-api/node_modules/#keystonejs/keystone/lib/providers/listAuth.js:250:13)"]}},"uid":"ckwqtreql0016z9sl2s81af6w","name":"GraphQLError"}],"data":{"authenticate":null},"extensions":{"tracing":{"version":1,"startTime":"2021-12-03T20:13:44.762Z","endTime":"2021-12-03T20:13:44.926Z","duration":164684813,"execution":{"resolvers":[{"path":["authenticate"],"parentType":"Mutation","fieldName":"authenticateUserWithPassword","returnType":"authenticateUserOutput","startOffset":2469132,"duration":159500839}]}}}}
I found the answer eventually.
You have to provide an extra object in your body called variables
variables: {
var1: "value1",
var2: "value2"
}
Those variables will then replace the placehodlers in the query like $var1 or $var2
Here is the full fetch code that works.
fetch("http://localhost:4000/admin/api", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `mutation signin($identity: String, $secret: String) {
authenticate: authenticateUserWithPassword(email: $identity, password: $secret) {
item {
id
}
}
}`,
variables: {
identity: "myusername",
secret: "mypassword"
}
})
}).then(res => res.json())
.then(data => {
console.log(data)
})
It's a shame that KeystoneJS don't provide any full code examples in their documentation. It would have saved me hours of searching.
As you say #PrestonDocks, if your query defines variables, you need to supply the variable values in a separate top level object. For the benefit of others I'll link to the GraphQL docs on this.
The alternative is to not use variables and to in-line your values in the query itself, like this:
mutation signin {
authenticate: authenticateUserWithPassword(
email: "me#example.com",
password: "Reindeer Flotilla"
) {
item {
id
}
}
}
But variables usually end up being neater.

Adding basic auth to all requests in Cypress

I'm a Cypress newbie and need to add basic auth to all cy.visit() calls.
The auth credentials are dependent on the deployment (i.e. they are specific to the 'baseUrl' which we set in the environment config files).
Currently, I have;
cy.visit("/", {
auth: {
username: '...',
password: '...'
}
});
What I want is to move the 'auth' object to the evg config files so I only need cy.visit("/") in the specs.
Many thanks
If you plan to reuse the authentification then is better to create a separate method for authentication e.g.:
1. Create a new custom command in `cypress/support/commands.js,
since it is loaded before any test files are evaluated via an import statement in your supportFile (cypress/support/index.js by default).
Cypress.Commands.add('login', () => {
// (you can use the authentification via API request)
return cy
.request({
method: 'POST',
url: your_url,
form: true,
body: {
username: Cypress.env('username'),
password: Cypress.env('password'),
grant_type: 'password',
client_id: your_clientId,
client_secret: your_clientSecret,
scope: 'openid',
},
})
})
2. Then use it in your test:
describe('My Test Name', () => {
before(() => {
cy.login();
});
it('should visit my base URL', () => {
cy.visit('/');
});
});
Note 1: Check how to set the environment variables here: Cypress.io: Environments Variables
Note 2: Check how to use the custom commands here: Custom Commands - Correct Usage
EDIT: since your syntax is correct - I will just share a way I use to do it in my tasks.
If your auth is working correctly you can make custom command - visitAndAuthorise like this for example:
Cypress.Commands.add("shopAdmin_visitAndLogin", (url) => {
cy.log('shopAdmin_visitAndLogin')
cy.visit(url)
cy.get('[data-qa="emailInput"]')
.type(Cypress.env('credentials').email)
cy.get('[data-qa="passwordInput"]')
.type(Cypress.env('credentials').password)
cy.get('[data-qa="loginButton"]')
.click()
cy.get('[data-qa="logOutButton"]')
.should('be.visible')
})
And your cypress.env.json file would need to include an object for the credentials like this:
{
"credentials": {
"email": "myEmail#gmail.com",
"password": "myPassword"
}
}
Or following your syntax:
Cypress.Commands.add("shopAdmin_visitAndLogin", (url) => {
cy.log('shopAdmin_visitAndLogin')
cy.visit(url, {
auth: {
username: Cypress.env('credentials').username,
password: Cypress.env('credentials').password
}})
})
If you have HTTP basic auth for all pages add this code to your cypress/support/commands.js:
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
options = options || {}
options.auth = {
username: 'replace_this_with_the_username',
password: 'replace_this_with_the_password'
}
return originalFn(url, options);
});
This is how I handled Basic Auth with Cypress using cy.request:
cy.request({
method:'POST',
url:'myURL',
body: {
Name: name,
userId: userId,
languageId: languageId
},
headers: {
authorization: 'Basic lasdkfjlsdyZHRoYXRpa25vdzp'
},
}).then(response => {
expect(response.status).to.equal(201)
})
})
Basically, the "headers" object inside the cy.request do the magic.

Send silent request to refresh tokens

I am using Vue.js in frontend and JWT based authentication system. I have refresh tokens and access tokens. Access tokens have short amount of expiration time whereas refresh tokens have way longer. I want to send a request to server to refresh user's access token silently. I know when the access token will be expired. I want to refresh it 1 minute, or something, before it expires. How can I implement this? I thought to do it with putting a counter to my root component but I have no an exact solution. Thanks.
I have a similar problem as you do and found this Vue JWT Auth in the same search that pulled your answer. Implementation was a little more challenging than I had originally anticipated.
My application needs to have the JWT tokens refresh on page reloads and immediately before API calls. To do this I use axios to consume the APIs, allowing the use of an interceptor to check the validity of the tokens. To keep the UX smooth, I use the vuex store to maintain the tokens, escalating to localStorage, and then to making an external request for new tokens if each previous stage was not successful.
The components outside of the store look like:
src/utils/apiAxios.js: used to consume APIs
import axios from 'axios'
import config from '../../config'
import store from '../store'
const apiAxios = axios.create({
baseURL: `${config.dev.apiURL}api/`,
timeout: 1000,
headers: {'Content-Type': 'application/json'}
})
// before any API call make sure that the access token is good
apiAxios.interceptors.request.use(function () {
store.dispatch('isLoggedIn')
})
export default apiAxios
To src/main.js added these lines:
import store from './store'
router.beforeEach((to, from, next) => {
let publicPages = ['/auth/login/', '/auth/register/']
let authRequired = !publicPages.includes(to.path)
let loggedIn = store.dispatch('isLoggedIn')
if (authRequired && !loggedIn) {
return next('/auth/login/')
}
next()
})
src/store/index.js:
import Vue from 'vue'
import Vuex from 'vuex'
import createLogger from 'vuex/dist/logger'
import auth from './modules/auth'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
modules: {
auth
},
strict: debug,
plugins: debug ? [createLogger()] : []
})
src/store/modules/auth.js:
import axios from 'axios'
import jwtDecode from 'jwt-decode'
import router from '../../utils/router'
import apiAxios from '../../utils/apiAxios'
import config from '../../../config'
export default {
state: {
authStatus: '',
jwt: {
refresh: '',
access: ''
},
endpoints: {
obtainJWT: config.dev.apiURL + 'auth/',
refreshJWT: config.dev.apiURL + 'auth/refresh/',
registerJWT: config.dev.apiURL + 'auth/register/',
revokeJWT: config.dev.apiURL + 'auth/revoke/',
verifyJWT: config.dev.apiURL + 'auth/verify/'
}
},
mutations: {
UPDATE_TOKEN (state, newToken) {
apiAxios.defaults.headers.common['Authorization'] = `Bearer ${newToken.access}`
localStorage.setItem('jwtAccess', newToken.access)
localStorage.setItem('jwtRefresh', newToken.refresh)
state.authStatus = 'success'
state.jwt = newToken
},
UPDATE_STATUS (state, statusUpdate) {
state.authStatus = statusUpdate
},
REVOKE_TOKEN (state) {
delete apiAxios.defaults.headers.common['Authorization']
localStorage.removeItem('jwtAccess')
localStorage.removeItem('jwtRefresh')
state.authStatus = ''
state.jwt = {
refresh: '',
access: ''
}
}
},
getters: {
authStatus: state => state.authStatus,
isLoggedIn: getters => {
// quick check of the state
return getters.authStatus === 'success'
}
},
actions: {
login ({ state, commit }, { email, password }) {
axios({
url: state.endpoints.obtainJWT,
method: 'POST',
data: {
email: email,
password: password
},
headers: {'Content-Type': 'application/json'}
})
.then((response) => {
commit('UPDATE_TOKEN', response.data)
})
.catch((error) => {
commit('UPDATE_STATUS', error)
console.log(error)
})
},
register ({ state, commit }, { email, password, firstName, lastName }) {
axios({
url: state.endpoints.registerJWT,
method: 'POST',
data: {
email: email,
password: password,
first_name: firstName,
last_name: lastName
},
headers: {'Content-Type': 'application/json'}
})
.then((response) => {
commit('UPDATE_TOKEN', response.data)
})
.catch((error) => {
commit('UPDATE_STATUS', error)
console.log(error)
})
},
logout ({ state, commit }) {
let refresh = localStorage.getItem('jwtRefresh')
axios({
url: state.endpoints.revokeJWT,
method: 'POST',
data: { token: refresh },
headers: {'Content-Type': 'application/json'}
})
.then(commit('REVOKE_TOKEN'))
.catch((error) => {
commit('UPDATE_STATUS', error)
console.log(error)
})
},
refreshTokens ({ state, commit }) {
let refresh = localStorage.getItem('jwtRefresh')
axios({
url: state.endpoints.refreshJWT,
method: 'POST',
data: {refresh: refresh},
headers: {'Content-Type': 'application/json'}
})
.then((response) => {
this.commit('UPDATE_TOKEN', response.data)
})
.catch((error) => {
commit('UPDATE_STATUS', error)
console.log(error)
})
},
verifyToken ({ state, commit, dispatch, getters }) {
let refresh = localStorage.getItem('jwtRefresh')
if (refresh) {
axios({
url: state.endpoints.verifyJWT,
method: 'POST',
data: {token: refresh},
headers: {'Content-Type': 'application/json'}
})
.then(() => {
// restore vuex state if it was lost due to a page reload
if (getters.authStatus !== 'success') {
dispatch('refreshTokens')
}
})
.catch((error) => {
commit('UPDATE_STATUS', error)
console.log(error)
})
return true
} else {
// if the token is not valid remove the local data and prompt user to login
commit('REVOKE_TOKEN')
router.push('/auth/login/')
return false
}
},
checkAccessTokenExpiry ({ state, getters, dispatch }) {
// inspect the store access token's expiration
if (getters.isLoggedIn) {
let access = jwtDecode(state.jwt.access)
let nowInSecs = Date.now() / 1000
let isExpiring = (access.exp - nowInSecs) < 30
// if the access token is about to expire
if (isExpiring) {
dispatch('refreshTokens')
}
}
},
refreshAccessToken ({ dispatch }) {
/*
* Check to see if the server thinks the refresh token is valid.
* This method assumes that the page has been refreshed and uses the
* #verifyToken method to reset the vuex state.
*/
if (dispatch('verifyToken')) {
dispatch('checkAccessTokenExpiry')
}
},
isLoggedIn ({ getters, dispatch }) {
/*
* This method reports if the user has active and valid credentials
* It first checks to see if there is a refresh token in local storage
* To minimize calls it checks the store to see if the access token is
* still valid and will refresh it otherwise.
*
* #isLoggedIn is used by the axios interceptor and the router to
* ensure that the tokens in the vuex store and the axios Authentication
* header are valid for page reloads (router) and api calls (interceptor).
*/
let refresh = localStorage.getItem('jwtRefresh')
if (refresh) {
if (getters.isLoggedIn) {
dispatch('checkAccessTokenExpiry')
} else {
dispatch('refreshAccessToken')
}
return getters.isLoggedIn
}
return false
}
}
}
I'm using django for my backend and django-rest-framework-simplejwt for the tokens. The returned JSON is formatted like:
{
access: "[JWT string]",
refresh: "[JWT string]"
}
with a token structure of:
header:
{
"typ": "JWT",
"alg": "HS256"
}
payload:
{
"token_type": "access",
"exp": 1587138279,
"jti": "274eb43bc0da429a825aa30a3fc23672",
"user_id": 1
}
When accessing the refresh endpoint, SimpleJWT requires in the data the refresh token be named refresh; for the verification and the revocation (blacklisting) endpoints the refresh token needs to be named token. Depending on what you are using for your backend will require modification from what I did.
The access token is only used in the api Authentication header and is updated when the mutations are called.
To get the token so I could decode it I used a simple shell script:
#!/usr/bin/env bash
EMAIL="my#email.com"
PASSWORD="aReallyBadPassword"
echo "API Login Token"
JSON_FMT='{"email":"%s","password":"%s"}'
JSON_FMT=` printf "$JSON_FMT" "$EMAIL" "$PASSWORD" `
curl \
--request POST \
--header Content-Type:application/json \
--data $JSON_FMT \
http://localhost:8000/api/auth/
echo ""

How to make api call by passing basic auth in axios in react

I have a api in which for login it works like this. To make api call i have to add authorization basic auth like this.
And the body
But the problem is when i make api request through axios it always give me response error like this
Error: "Request failed with status code 401"
createError createError.js:17
settle settle.js:19
handleLoad xhr.js:60
I made request something like this.
//TODO: AUTH Actions
export const login = (username, password) => (dispatch) => {
//User Loading:
dispatch({ type: USER_LOADING });
//Make API Call here
console.log('featch adata', data);
axios.post('https://api.smartocart.com/oauth/token', {
data: {
username: username,
password: password,
grant_type: 'password',
},
auth: {
username: 'topseller',
password: 'topseller'
}
})
.then((res) => {
console.log('Then response', res)
}).catch(err => {
console.log(err)
})
// console.log('Action login', username, password);
}
As mentioned try the config object as third param to .post function, Request post body should be the second param.
https://github.com/axios/axios#axiosposturl-data-config-1
You are trying something like axios.post(url, config) but such a function is not there. You should use axios.post(url, data, config)
axios.post('https://api.smartocart.com/oauth/token',
{
username: username,
password: password,
grant_type: 'password',
},
{
auth: {
username: 'topseller',
password: 'topseller'
}
}
).then((res) => {
console.log('Then response', res)
}).catch(err => {
console.log(err)
})

globalize axios as API wrapper in vue project

I have almost 13 Axios requests in my Vue application. which are almost the same
axios({
method: 'post',
url: `${this.$root.api_url}/v2/cameras/${this.selected.exid}/nvr/snapshots/extract`,
data: {
start_date: moment(this.fromDateTime).format(),
end_date: moment(this.toDateTime).format(),
schedule: this.schedule,
interval: this.interval,
create_mp4: this.create_mp4,
inject_to_cr: this.inject_to_cr,
jpegs_to_dropbox: this.jpegs_to_dropbox,
requester: this.$root.user.email,
api_key: this.selected.api_key,
api_id: this.selected.api_id
}
}).then(response => {
if (response.status == 201) {
this.showSuccessMsg({
title: "Success",
message: "Snapshot Extractor has been added (Local)!"
});
this.$events.fire('se-added', {})
this.clearForm()
} else {
this.showErrorMsg({
title: "Error",
message: "Something went wrong!"
})
}
})
I pass the method, URL and data.. and do a few things in response and in case of error.
How can I reduce that so much code? I have this idea to make an API file for this where, the method will accept, API.get(method, URL, data) and I will have {message, statusCode} in return. and then on the basis of that, I can do other stu7ff.
I tried to follow some documentation online but it didn't work. Is there any suitable way to reduce this code.
Is it even possible to give success and error message as well in API.get or post or delete that it would be very minimal when you send the API request?
EDIT: so i guess you need something like a class here:
class API {
static get(url, callback) {
axios({
method: "get",
url: url,
data: data
}).then(response => {
callback(response);
});
}
static post(url, data, callback) {
axios({
method: "post",
url: url,
data: data
}).then(response => {
callback(response);
});
}
}
API.post("url", data, response => {
console.log(response);
});
API.get("url", response => {
console.log(response);
});
I use yamlful
You make a .yml file which includes
events:
- method: get
get: /events/:id
then API calls become
const response = await this.$api.events.get(2)
Furthermore, I inject methods into my context
// api.js
async function populateEvents (app, id) {
const response = await app.$api.events.get(id)
return response
}
export default ({ app, store }, inject) => {
inject('populateEvents', id => populateEvents(app, id))
}
// any_file.vue
this.populateEvents(12)
and in api.js you can generalize your api calls, so if any 2 api calls do the same stuff, you can refactor that repeated code into a separate method