I'm trying to automate a login process for our site which uses Auth0 & Google Sign in. On the login page if you click Google sign in you get sent to an Auto0 page with a form and another Google sign in link, the page contains a URL something like:
https://OURDOMAIN.auth0.com/login?state=*REMOVED*
It's the first time I'm trying to use cy.origin() In my test I'm trying this:
cy.get("a[testid='googleSignInButton']").click()
cy.origin('https://OURDOMAIN.auth0.com/', () => {
cy.get("input[type='email']").type('anEmail#me.com')
})
The problem is whatever I try to do in the origin block just returns a timeout trying to find the element.
I've set experimentalSessionAndOrigin: true is there something I'm doing wrong? Unfortuantly the way we are using Auth0 and Google Sign in means it's not possible to do it via API calls.
try without cy.origin()
I had the same problem, and it was fixed by removing the cy.origin()
Try this one:
Cypress.Commands.add('loginSession', (email, password) => { cy.session([email, password], () => {
cy.visit('/').then(() => {
//cy.origin('auth0.com', { args: [email, password] }, ([email, password]) => {
cy.get('#username').type(email)
cy.get('#password').type(password)
cy.get("button[type='submit']").click({ force: true })
// })
})
cy.get('[data-cy="logo"]').should('be.visible')
})
});
Related
I am trying to bypass the UI login by using cy.request() to log a user in and cy.visit() to go to the restricted route. I have followed this doc: https://docs.cypress.io/guides/end-to-end-testing/testing-your-app#Bypassing-your-UI
However, the test fails because the visit URL ('http://localhost:3000/join-or-create') is not loaded and instead the home page URL is loaded (http://localhost:3000/).
This is my test code:
describe('when user is signed in but has not joined a group', () => {
beforeEach(() => {
cy.request('POST', 'http://localhost:5000/api/testing/reset');
const user = {
name: 'Joe Bloggs',
email: 'Joe#Bloggs.com',
password: 'Password',
};
cy.request('POST', 'http://localhost:5000/register', user);
cy.request('POST', 'http://localhost:5000/sign-in', user);
cy.visit('http://localhost:3000/join-or-create');
});
it.only('should allow a logged in user to join or create a group', () => {
cy.contains('Join a group');
cy.contains('Create a group');
});
});
If I change cy.contains('Join a group'); to cy.contains('Welcome'); (which is content on the URL 'http://localhost:3000/') then the test passes.
If I use:
cy.visit('http://localhost:3000');
cy.get('[data-testid="email"]').type('Joe#Bloggs.com');
cy.get('[data-testid="password"]').type('Password');
cy.contains('sign in').click();
instead of cy.visit('http://localhost:3000/join-or-create'); the test passes.
The output of the test body shows that is redirecting to a new URL 'http://localhost:3000/' (as shown in the screenshot below) but I can't figure out why.
Thanks for any help.
In the bypass code, check the response from the POST request
cy.request('POST', 'http://localhost:5000/sign-in', user)
.then(response => console.log(response))
to see what tokens are returned.
Then in the form-login code, look at what happens to the same token after cy.contains('sign in').click() and see where the browser stores same token.
That's probably the step missing in the bypass code. You'll need to add something to do the same, e.g
cy.request('POST', 'http://localhost:5000/sign-in', user)
.then(response => {
const token = response.body.token; // figure out where the token is in response
cy.setCookie(token); // figure out where the app want's the token
})
It's also difficult to tell from the question what URL you need to cy.visit() in the bypass code, but there's only a couple of them so try both permutations.
I have a website which needs browser authentication on all page visits. If I use the below code on all page visits it works fine:
describe('WW2-3461 validate patches for contrib modules and core', () => {
it('should visit my base URL', () => {
cy.visit(Cypress.env('base_url'), {
// Add basic auth headers
auth: {
username: Cypress.env('browserAuthentication').username,
password: Cypress.env('browserAuthentication').password
},
failOnStatusCode: false
})
})
})
I am passing the base_url as an environment through CLI:
npx cypress run --spec file.spec.js --env base_url=$base_url
I don't want to use browserauthentication on all page visits, so I created a custom command like below (in integration/project/utility.js file):
Cypress.Commands.add('addBrowserAuthentication', (url) => {
cy.visit(url, {
// Add basic auth headers
auth: {
username: Cypress.env('browserAuthentication').username,
password: Cypress.env('browserAuthentication').password
},
failOnStatusCode: false
})
})
However if I call this command inside a spec file, like below:
import './utility.spec'
describe('WW2-3461 validate patches for contrib modules and core', () => {
beforeEach(() => {
cy.addBrowserAuthentication(Cypress.env('cypress_host'))
})
it('should visit my base URL', () => {
cy.visit(Cypress.env('cypress_host'), {
})
})
the browser authentication does not seem to work (tried to call it inside before() hook or directly inside the test scenario) as it returns
> 401: Unauthorized
This was considered a failure because the status code was not `2xx`.
This http request was redirected '1' time to:
- 302: https://mysite.local
If you do not want status codes to cause failures pass the option: `failOnStatusCode: false`
I can't seem to work around this issue. Tried using cy.request() but getting the same issue. Has someone faced this issue?
You don't need to pass env variable as a parameter, you can directly access it inside the custom command.
Cypress.Commands.add('addBrowserAuthentication', () => {
cy.visit(Cypress.env('base_url'), {
// Add basic auth headers
auth: {
username: Cypress.env('browserAuthentication').username,
password: Cypress.env('browserAuthentication').password,
},
failOnStatusCode: false,
})
})
And in your test directly use:
cy.addBrowserAuthentication()
Also I am not sure if its required to visit the website two times. So you can remove cy.visit(Cypress.env('cypress_host') if you are already using cy.addBrowserAuthentication().
Since this works
cy.visit(Cypress.env('base_url'), {
// Add basic auth headers
auth: {
username: Cypress.env('browserAuthentication').username,
password: Cypress.env('browserAuthentication').password
},
failOnStatusCode: false
})
but this doesn't
cy.addBrowserAuthentication(Cypress.env('cypress_host'))
it looks like Cypress.env('cypress_host') is not the same as Cypress.env('base_url').
Everything else looks good in your command, so I'd try with
cy.addBrowserAuthentication(Cypress.env('base_url'))
When you cy.visit(Cypress.env('cypress_host') without headers inside the test, the auth headers from addBrowserAuthentication are lost. I suggest removing that cy.visit().
I am having problem bypassing UI login. My web application doesn't use API to authenticate users. There are no endpoints like /login. index.php will just open the login page and submit the form to login.
The application authenticate the user by
auth($_REQUEST['username'], $_REQUEST['password_tx']);
This is what cypress printed after UI login submit.
I have no idea how to move on from here.
// This doesn't work. The application doesn't get the user details from the body. It is in the submitted form.
cy.request({
method: 'POST',
url: '/index.php?p=sys001',
form: true,
body: {
username: 'user',
password_tx: 'pass'
}
})
This is the complete testcase for the issue. Added comments to make them understandable.
it("login via form spoof", () => {
cy.get("div#mDiv > form").invoke("attr", "action").then(($action) => { //get
the attribute of 'action' and pass encoded uname and pwd to it
let username = Cypress.env("username");
let password = Cypress.env("password");
cy.intercept("POST", $action, (req) => { //post request and populate body
// intercepting the POST form to spoof it.
req.body = $action + encodeURIComponent(username)+ encodeURIComponent(password)
})
.as("loginForm"); //alias
});
cy.get("div#mDiv > div.login > form")
.submit(); //Submit the form after locating it.
});
I'm wondering if it is possible to save the state of localStorage across tests.
Mainly because I want to avoid re-authentication on each test. I realize that I can create a command that sends an API request to our backend to avoid going through the auth flow but for various reasons this won't work in my situation.
I am asking if it possible to have a workflow like this:
Go to login page: Authenticate get back response and save session to local storage
Persist local storage somehow...
Go through other tests as authenticated user
You can use the cypress-localstorage-commands package to persist localStorage between tests, so you'll be able to do login only once:
In support/commands.js:
import "cypress-localstorage-commands";
In your tests:
before(() => {
// Do your login stuff here
cy.saveLocalStorage();
});
beforeEach(() => {
cy.restoreLocalStorage();
});
Here's what I ended up doing:
Go to login page: Authenticate
At this point we have data we want to persist between tests in localStorage but we are not allowed to whitelist localStorage.
However, we are allow to whitelist cookies
I have some code like this inside my support/commands.js that act as helpers
const sessionKeys = {
authTokens: 'auth.tokens',
sessionConfig: 'session.config',
};
// The concatenation of username and cid will be the key to set the session
Cypress.Commands.add('persistSession', (key) => {
const authTokens = localStorage.getItem(key);
cy.setCookie(key, authTokens);
});
Cypress.Commands.add('restoreSession', (key) => {
cy.getCookie(key).then(authTokens => {
localStorage.setItem(key, authTokens.value);
});
});
So we call cy.persistSession(key) after we login, which means we have all the authentication saved as cookies which are whitelisted inside of support/index.js with code.
Like this:
Cypress.Cookies.defaults({
whitelist: function(cookie){
// Persist auth stuff
const reAuthTokens = new RegExp('.*auth\.tokens');
if(reAuthTokens.test(cookie.name)){
return true;
}
return false;
}
});
Now anytime we need our auth tokens inside our other tests before running them we cy.restoreSession(key) and we should be good!
Here is the useful link that solves my problem like yours: Preserve cookies through multiple tests
my code like:
const login = () => {
cy.visit('http://0.0.0.0:8080/#/login');
cy.get('#username').type('username');
cy.get('#password').type('1234password$');
cy.get('#login-button').click();
}
describe('UI', () => {
// beforeEach(login);
beforeEach(() => {
login();
Cypress.Cookies.preserveOnce('session_id', 'remember_token');
});
});
hope can help you.
Anything you can do in JS you can do in a cypress test. If you have some way to store creds (auth token, etc.) in local storage, I see no reason why you can't do that. If cypress is clearing out your local storage between tests, you will have to write a beforeEach hook that saves an authenticated token (hard-coded by you) to local storage before each test.
I can't seem to find any documentation on how to restrict the login to my web application (which uses OAuth2.0 and Google APIs) to only accept authentication requests from users with an email on a specific domain name or set of domain names. I would like to whitelist as opposed to blacklist.
Does anyone have suggestions on how to do this, documentation on the officially accepted method of doing so, or an easy, secure work around?
For the record, I do not know any info about the user until they attempt to log in through Google's OAuth authentication. All I receive back is the basic user info and email.
So I've got an answer for you. In the OAuth request you can add hd=example.com and it will restrict authentication to users from that domain (I don't know if you can do multiple domains). You can find hd parameter documented here
I'm using the Google API libraries from here: http://code.google.com/p/google-api-php-client/wiki/OAuth2 so I had to manually edit the /auth/apiOAuth2.php file to this:
public function createAuthUrl($scope) {
$params = array(
'response_type=code',
'redirect_uri=' . urlencode($this->redirectUri),
'client_id=' . urlencode($this->clientId),
'scope=' . urlencode($scope),
'access_type=' . urlencode($this->accessType),
'approval_prompt=' . urlencode($this->approvalPrompt),
'hd=example.com'
);
if (isset($this->state)) {
$params[] = 'state=' . urlencode($this->state);
}
$params = implode('&', $params);
return self::OAUTH2_AUTH_URL . "?$params";
}
I'm still working on this app and found this, which may be the more correct answer to this question. https://developers.google.com/google-apps/profiles/
Client Side:
Using the auth2 init function, you can pass the hosted_domain parameter to restrict the accounts listed on the signin popup to those matching your hosted_domain. You can see this in the documentation here: https://developers.google.com/identity/sign-in/web/reference
Server Side:
Even with a restricted client-side list you will need to verify that the id_token matches the hosted domain you specified. For some implementations this means checking the hd attribute you receive from Google after verifying the token.
Full Stack Example:
Web Code:
gapi.load('auth2', function () {
// init auth2 with your hosted_domain
// only matching accounts will show up in the list or be accepted
var auth2 = gapi.auth2.init({
client_id: "your-client-id.apps.googleusercontent.com",
hosted_domain: 'your-special-domain.example'
});
// setup your signin button
auth2.attachClickHandler(yourButtonElement, {});
// when the current user changes
auth2.currentUser.listen(function (user) {
// if the user is signed in
if (user && user.isSignedIn()) {
// validate the token on your server,
// your server will need to double check that the
// `hd` matches your specified `hosted_domain`;
validateTokenOnYourServer(user.getAuthResponse().id_token)
.then(function () {
console.log('yay');
})
.catch(function (err) {
auth2.then(function() { auth2.signOut(); });
});
}
});
});
Server Code (using googles Node.js library):
If you're not using Node.js you can view other examples here: https://developers.google.com/identity/sign-in/web/backend-auth
const GoogleAuth = require('google-auth-library');
const Auth = new GoogleAuth();
const authData = JSON.parse(fs.readFileSync(your_auth_creds_json_file));
const oauth = new Auth.OAuth2(authData.web.client_id, authData.web.client_secret);
const acceptableISSs = new Set(
['accounts.google.com', 'https://accounts.google.com']
);
const validateToken = (token) => {
return new Promise((resolve, reject) => {
if (!token) {
reject();
}
oauth.verifyIdToken(token, null, (err, ticket) => {
if (err) {
return reject(err);
}
const payload = ticket.getPayload();
const tokenIsOK = payload &&
payload.aud === authData.web.client_id &&
new Date(payload.exp * 1000) > new Date() &&
acceptableISSs.has(payload.iss) &&
payload.hd === 'your-special-domain.example';
return tokenIsOK ? resolve() : reject();
});
});
};
When defining your provider, pass in a hash at the end with the 'hd' parameter. You can read up on that here. https://developers.google.com/accounts/docs/OpenIDConnect#hd-param
E.g., for config/initializers/devise.rb
config.omniauth :google_oauth2, 'identifier', 'key', {hd: 'yourdomain.com'}
Here's what I did using passport in node.js. profile is the user attempting to log in.
//passed, stringified email login
var emailString = String(profile.emails[0].value);
//the domain you want to whitelist
var yourDomain = '#google.com';
//check the x amount of characters including and after # symbol of passed user login.
//This means '#google.com' must be the final set of characters in the attempted login
var domain = emailString.substr(emailString.length - yourDomain.length);
//I send the user back to the login screen if domain does not match
if (domain != yourDomain)
return done(err);
Then just create logic to look for multiple domains instead of just one. I believe this method is secure because 1. the '#' symbol is not a valid character in the first or second part of an email address. I could not trick the function by creating an email address like mike#fake#google.com 2. In a traditional login system I could, but this email address could never exist in Google. If it's not a valid Google account, you can't login.
Since 2015 there has been a function in the library to set this without needing to edit the source of the library as in the workaround by aaron-bruce
Before generating the url just call setHostedDomain against your Google Client
$client->setHostedDomain("HOSTED DOMAIN")
For login with Google using Laravel Socialite
https://laravel.com/docs/8.x/socialite#optional-parameters
use Laravel\Socialite\Facades\Socialite;
return Socialite::driver('google')
->with(['hd' => 'pontomais.com.br'])
->redirect();