KeystoneJS login via GraphQL mutation - authentication

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.

Related

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

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

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.

apollo client query with params - Vue

Apollo-client's query:
apolloClient
.query({
query: authQuery,
variables: {
login: payload.login,
password: payload.password,
},
})
.then((res) => console.log(res)
Contents of authQuery (it's inside a file.gql):
query auth {
data
}
i always get the following response:
{
data: null
loading: false
networkStatus: 7
stale: true
}
Though in graphiQL i am getting correct response:
Query in a GraphiQL
{
auth(login:"root#admin", password:"1234")
}
And response:
{
"data": {
"auth": "eyJhbGciOi8"
}
}
I suspect my file.gql is the culprit? Or variables inside query not being read?
I had to wrap my query this way:
query Auth($login: String!, $password: String!) {
auth (login: $login, password: $password)
}
When you're using params with your Query, you always have to wrap it this way.

React Native: Ajax error with RxJS on simple post request

I am trying to perform a simple post request in React Native with a module that I also use for my website.
I have an api.ts file where the following is defined:
import { ajax } from 'rxjs/observable/dom/ajax';
import { AjaxRequest } from 'rxjs/observable/dom/AjaxObservable';
const ApiClient = {
loginUser: (email: string, password: string) => {
let requestBody = {email, password};
let url = `${dotenv.REACT_APP_API_URL}/api/users/login`;
return createRequestOptions(url, HttpOptions.POST, requestBody);
}
}
The request options method is as follows:
const createRequestOptions = (url: string, method: string, requestBody?: object) => {
let requestOptions: AjaxRequest = {
method: method,
url: url,
crossDomain: true,
responseType: 'json',
headers: {
'Content-Type': 'application/json',
}
};
if ((method === HttpOptions.POST || method === HttpOptions.PUT) && requestBody) {
requestOptions.body = requestBody;
}
console.log(requestOptions);
return ajax(requestOptions);
};
The output of the requestOptions is as follows:
Object {
"body": Object {
"email": "myemail#gmail.com",
"password": "mypassword",
},
"crossDomain": true,
"headers": Object {
"Content-Type": "application/json",
},
"method": "POST",
"responseType": "json",
"url": "http://localhost:3001/api/users/login",
}
Finally in my epic I have the following:
const authLoginEpic: Epic = (action$, store) =>
action$.pipe(
ofType(ActionTypes.AUTH_LOGIN_REQUEST),
mergeMap((action: AuthLoginRequest) =>
ApiClient.loginUser(action.payload.username, action.payload.password).pipe(
map((res: any) => {
return AuthLoginReceive.create({response: res.response, email: action.payload.username});
}),
catchError((err) => {
console.log(JSON.stringify(err));
For some reason the catchError is triggered and I have no idea why this may be. The output of the log is:
{"message":"ajax error","name":"AjaxError","xhr":{"UNSENT":0,"OPENED":1,"HEADERS_RECEIVED":2,"LOADING":3,"DONE":4,"readyState":4,"status":0,"timeout":0,"withCredentials":false,"upload":{},"_aborted":false,"_hasError":true,"_method":"POST","_response":"","_url":"http://localhost:3001/api/users/login","_timedOut":false,"_trackingName":"unknown","_incrementalEvents":false,"_requestId":null,"_cachedResponse":null,"_headers":{"content-type":"application/json"},"_responseType":"json","_sent":true,"_lowerCaseResponseHeaders":{},"_subscriptions":[]},"request":{"async":true,"crossDomain":true,"withCredentials":false,"headers":{"Content-Type":"application/json"},"method":"POST","responseType":"json","timeout":0,"url":"http://localhost:3001/api/users/login","body":"{\"email\":\"mymail#gmail.com\",\"password\":\"mypassword\"}"},"status":0,"responseType":"json","response":null}
The Ajax error is not very descriptive. Does anyone what I may be doing wrong?
It seems that this happened due to the simple fact that the api address was set to localhost or 127.0.0.1
Ensure to have set this to your local network IP address!

Backand: Sign user in immediately after registering?

Having trouble doing this - is it even possible?
Sign-up Email Verification is off, and I'm doing this in the config:
BackandProvider.setAppName( 'test' );
BackandProvider.runSigninAfterSignup( true );
// ... tokens, etc.
Getting this back in the response after hitting the /1/user/signup endpoint:
data : {
currentStatus : 1,
listOfPossibleStatus : [...],
message : "The user is ready to sign in",
token : "...",
username : "tester#test.com"
}
Do I need to make another API call? Can't find where and with which params.
Yes, you must make another API call to get token after sign up. If you use the Backand SDK by default it makes the second call.
$scope.signup = function (form) {
return Backand.signup(form.firstName, form.lastName,
form.username, form.password,
form.password,
{company: form.company})
.then(function (response) {
$scope.getUserDetails();
return response;
});
};
If you lool at the SDK code, this is what happens there:
self.signup = function (firstName, lastName, email, password, confirmPassword, parameters) {
return http({
method: 'POST',
url: config.apiUrl + urls.signup,
headers: {
'SignUpToken': config.signUpToken
},
data: {
firstName: firstName,
lastName: lastName,
email: email,
password: password,
confirmPassword: confirmPassword,
parameters: parameters
}
}).then(function (response) {
$rootScope.$broadcast(EVENTS.SIGNUP);
if (config.runSigninAfterSignup
&& response.data.currentStatus === 1) {
return self.signin(email, password);
}
return response;
})
};