Protractor not waiting for login redirect before continuing tests in AngularJS, any suggestion? - authentication

I have a standard username/password/submit button form, when the user clicks on the button the form submits with ng-submit="login.submit()" which does the login and on success redirects to the main page using ui.router ($state.go("main")).
The following test fails:
describe("login", function() {
beforeEach(function() {
var email = element(by.model("login.email"));
email.clear().sendKeys("mail");
var password = element(by.model("login.password"));
password.clear().sendKeys("pass");
var submit = element(by.id("submit"));
submit.click();
});
it("should be able to login", function() {
expect(element(by.css(".loginPage")).isPresent()).toBe(false);
expect(element(by.css(".mainPage")).isPresent()).toBe(true);
});
});
and if I try to add wait times around, I can see that the browser stays on the login page the whole time (after clicking on the button) - then I get a timeout.
After a successful login the browser receives a cookie with a token for authenticating each following request.
EDIT: with some tinkering I found out where it fails..
function login(email, pass) {
alert("it gets here");
return _auth.post({ username: email, password: pass }).then(function(data) {
alert("does not get here");
console.log("loginok, token:" +$browser.cookies().apiToken); //this should be the received token
return data;
});
}
EDIT2: the Auth service
var _auth = Restangular.withConfig(function(Configurer) {
Configurer.setBaseUrl("/");
}).service("auth/simple");
return {
login: login,
};
function login(email, pass) {
return _auth.post({ username: email, password: pass });
}
Manually everything works as expected.

#JoMendez's answer was very close but didn't work in my case. Used #DaveGray's here.
Had to wrap the isPresent() call in a function.
browser.wait(function() {
return element(by.css('.mainPage')).isPresent();
});

Try this:
it("should be able to login", function() {
browser.wait(element(by.css(".mainPage")).isPresent);//this is different from sleep, this will stop the excecution of all the protractor code that is after it, until the element is present, but it won't prevent the application of loading or if is redirecting, it will keep working.
expect(element(by.css(".loginPage")).isPresent()).toBe(false);
expect(element(by.css(".mainPage")).isPresent()).toBe(true);
});
});

Related

Toast Notification not triggering .NET with Angular

Hope someone can help me clarify some doubts I have regarding popups and toasts. I have a .ASPNET Core application with users, I have a change password functionality and i'm trying to send a toastr.success message whenever the password is changed without an issue, how could I do this?
Currently, I have this method when the form is submited:
onSubmit() {
this.accountService.changePassword(this.updatePasswordForm.value).subscribe(response => {
}, error => {
this.validationErrors = error
})
From the component, the form value travels to the account service:
changePassword(model: any) {
this.router.navigateByUrl('/member/edit');
return this.http.post(this.baseUrl + 'account/changePassword', model);
And finally from here, to the controller:
[HttpPost("changePassword")]
public async Task<ActionResult<ChangePasswordDTO>> ChangePassword([FromBody] ChangePasswordDTO changePassword) {
//GET CURRENTLY LOGGED IN USER
var currentUserId = this.User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _userManager.FindByIdAsync(currentUserId);
//CHANGE PASSWORD USING USERMANAGER
var result = await _userManager.ChangePasswordAsync(user, changePassword.OldPassword, changePassword.NewPassword);
if (!result.Succeeded) {
return Unauthorized("There was an error changing your password, please try again.");
} else return Ok("Password changed successfull");
When the controller fails to change the password, a toast pops up with the unauthorized message, but on success, nothing happens except the redirect and a console log with a 200 OK message. How and where could I add a success toast if the password is changed successfully?
Thank you for your help in advance!

Vue + MSAL2.x + Azure B2C Profile Editing

First, I am not finding Vue specific examples using MSAL 2.x and we'd like to use the PKCE flow. I am having issues with the way the router guards are run before the AuthService handleResponse so I must be doing something wrong.
In my main.js I am doing this...
// Use the Auth services to secure the site
import AuthService from '#/services/AuthServices';
Vue.prototype.$auth = new AuthService()
And then in my AuthConfig.js I use this request to login:
loginRequest : {
scopes: [
"openid",
"profile",
process.env.VUE_APP_B2C_APISCOPE_READ,
process.env.VUE_APP_B2C_APISCOPE_WRITE
]
},
The docs say it should redirect to the requesting page but that is not happening. If user goes to the protected home page they are redirected to login. They login, everything is stored properly so they are actually logged in, but then they are sent back to the root redirect URL for the site, not the Home page.
When a user wants to login we just send them to the protected home page and there is a login method called in the router guard which looks like this:
router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const IsAuthenticated = await Vue.prototype.$auth.isAuthenticated()
console.log(`Page changing from ${from.name} to ${to.name}, requiresAuth = ${requiresAuth}, IsAuthenticated = ${IsAuthenticated}`)
if (requiresAuth && !IsAuthenticated)
{
next(false)
console.log('STARTING LOGIN')
Vue.prototype.$auth.login()
// Tried this
// Vue.prototype.$auth.login(to.path)
} else {
next()
}
})
In AuthServices.js I have this...
// The user wants to log in
async login(nextPg) {
// Tell B2C what app they want access to and their invitation ID if they are new
if (store.getters.userEmail != null) {
aCfg.loginRequest.loginHint = store.getters.userEmail
}
aCfg.loginRequest.state = "APP=" + store.getters.appCode
if (store.getters.appointmentLink != null && store.getters.appointmentLink != '') {
aCfg.loginRequest.state += ",ID=" + store.getters.appointmentLink
}
// Tried this
// if (nextPg && nextPg != '') {
// aCfg.loginRequest.redirectUrl = process.env.VUE_APP_B2C_REDIRECT_URL + nextPg
// }
return await this.msalInst.loginRedirect(aCfg.loginRequest)
}
I tried puting a nextPg parameter in the login method and adding a redirectUrl property to the login request but that gives me an error saying it is not one of the configured redirect URLs.
Also, I'm trying to make the user experience better when using the above technologies. When you look at the MSAL2.x SPA samples I see that when returning from a Profile Edit, a user is logged out and they are required to log in again. That sounds like a poor user experience to me. Sample here: https://github.com/Azure-Samples/ms-identity-b2c-javascript-spa/blob/main/App/authRedirect.js
Do I need to just create my own profile editing page and save data using MSGraph to prevent that? Sorry for the noob questions. Ideas?
Update - My workaround which seems cheesy is to add these two methods to my AuthService.js:
storeCurrentRoute(nextPath) {
if (!nextPath) {
localStorage[STOR_NEXT_PAGE] = router.history.current.path
} else {
localStorage[STOR_NEXT_PAGE] = nextPath
}
console.log('Storing Route:', localStorage[STOR_NEXT_PAGE])
}
reEstablishRoute() {
let pth = localStorage[STOR_NEXT_PAGE]
if (!!pth && router.history.current.path != pth) {
localStorage[STOR_NEXT_PAGE] = ''
console.log(`Current path is ${router.history.current.path} and reEstablishing route to ${pth}`)
router.push({ path: pth })
}
}
I call storeCurrentRoute() first thing in the login method and then in the handleResponse() I call reEstablishRoute() when its not returning from a profileEdit or password change. Seems like I should be able to make things work without this.
Update Number Two - When returning from B2C's ProfileEdit User Flow the MSAL component is not logging me out properly. Here is my code from my handlePolicyChange() method in my AuthService:
} else if (response.idTokenClaims[clmPolicy] === aCfg.b2cPolicies.names.editProfile) {
Vue.nextTick(() => {
console.log('BACK FROM Profile Change')
Vue.prototype.$swal(
"Success!",
"Your profile has been updated.<br />Please log in again.",
"success"
).then(async () => {
this.logout()
})
})
}
:
// The user wants to log out (all accounts)
async logout() {
// Removes all sessions, need to call AAD endpoint to do full logout
store.commit('updateUserClaims', null)
store.commit('updateUserEmail', null)
let accts = await this.msalInst.getAllAccounts()
for(let i=0; i<accts.length; i++) {
const logoutRequest = {
account: accts[i],
postLogoutRedirectUri: process.env.VUE_APP_B2C_REDIRECT_URL
};
await this.msalInst.logout(logoutRequest);
}
return
}
It is working fine until the call to logout() which runs without errors but I looked in my site storage (in Chrome's debug window > Application) and it looks like MSAL did not clear out its entries like it does on my normal logouts (which always succeed). Ideas?
As part of the MSAL auth request, send a state Parameter. Base64 encode where the user left off inside this parameter. MSAL exposes extraQueryParameters which you can put a dictionary object inside and send in the auth request, put your state Key value pair into extraQueryParameters.
The state param will be returned in the callback response, use it to send the user where you need to.

Authenticate with Moodle from a mobile app

My mobile app needs to log in to Moodle to get Json data from a webservice and display it using Angular.
In order to do that, I need to pass in a username and password and get a Moodle webservice token back, so my app doesn't need to log in again (at least until the token expires).
(this is one of those "ask and answer your own question" things, so my solution is below, but comments & suggestions welcome.)
With thanks to all the other StackOverflow pages I have used to create this solution!
See also - how to get data from your Moodle webservice with Angular.
Step 1. Check if a token already exists
jQuery(document).ready(function () {
/* when the user clicks log-out button, destroy the session */
$('#btn_logout').on('click', function () {
$('.pane').hide(); /* hide all screens */
$('#menu').toggleClass('ui-panel-open ui-panel-closed');
$.jStorage.deleteKey('session');
makeUserLogin();
});
var session = $.jStorage.get('session', ''); // syntax: $.jStorage.get(keyname, "default value")
if (session) { // if there is already a session, redirect to landing pane
showApp();
} else { // if there is no session *then* redirect to the login pane
makeUserLogin();
}
});
Step 2. create functions to show app & redirect to login page
function showApp() {
$('#home-pane').show(); /* show home screen */
$('#system-message').hide();
$('#login-pane').hide(); /* hide login screen*/
$('#menu_btn').removeClass('hidden'); /* show menu button so user can see rest of app */
}
function makeUserLogin() {
$('#btn_login').click(function () {
console.log('click event for login_button');
var username = $('#username').val();
var password = $('#password').val();
postCredentials(username, password, createSession);
});
$('#menu_btn').addClass('hidden'); /* hide menu button so user cannot see rest of app */
$('#home-pane').hide(); /* hide home screen */
$('#login-pane').show(); /* show login screen */
}
function postCredentials(username, password, callback) {
if ((username.length && password.length) && (username !== '' && password !='')) {
var url = 'https://moodle.yourcompany.com/local/login/token.php';
$.post(url, {
username: username,
password: password,
service: 'webservice_ws' // your webservice name
}).done(function (data) {
token = data.token;
dataString = JSON.stringify(data);
if (dataString.indexOf('error') > 0) {
showErrorDialog('<p class="error">Invalid user credentials, please try again</p>');
}
else {
createSession(token);
}
}).fail(function () {
showErrorDialog('<p class="error">Login failed</p>');
});
} else {
showErrorDialog('<p class="error">Please enter a username and password</p>');
}
}
function createSession(token) {
// syntax: $.jStorage.set('keyname', 'keyvalue', {TTL: milliseconds}); // {TTL... is optional time, in milliseconds, until key/value pair expires}
$.jStorage.set('session', token, { TTL: 28800000 });
// redirect to whatever page you need after a successful login
showApp();
}
function showErrorDialog(errorMsg) {
$('#system-message').html(errorMsg);
$('#system-message').fadeIn();
}

Auth0 callback URL mismatch

I am doing LinkedIn authentication with auth0 in a react app. I have set localhost:3000/upload in callback urls in settings, hopping that after users login at localhost:3000/login, they would be redirected to localhost:3000/upload. However, I always get this error: url localhost:3000/login is not in the list of callback urls. Why would auth0 expect to return to the page where you just logged in after logging in. Shouldn't it be some different url. It just does not make sense to me.
Edit:
export default class AuthService {
constructor(clientId, domain) {
// Configure Auth0
const options = {
allowedConnections: ['linkedin'],
auth: {
params: {responseType: 'code'}
}
};
this.lock = new Auth0Lock(clientId, domain, options)
// Add callback for lock `authenticated` event
this.lock.on('authenticated', this._doAuthentication.bind(this))
// binds login functions to keep this context
this.login = this.login.bind(this)
this.loggedIn = this.loggedIn.bind(this)
}
_doAuthentication(authResult){
// Saves the user token
console.log(authResult);
this.setToken(authResult.idToken)
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
console.log('Error loading the Profile', error)
} else {
console.log(profile)
}
})
}
//....
Please ensure two things:
1). In your react app code
responseType: 'code'
2). On the Auth0 dashboard, under Settings -> Allowed Callback URLs put your callback entry (localhost:3000/upload) - which I think you have done but just in case.
Let me know if you are still having problems.
Make sure that there is no special hidden characters or space between the commas between the URLs when you paste it into the Auth0 Setting site. I didn't realise about this util I put every urls into Vim to check and see that there are such above cases
In the call to AuthProvider, make sure to use to same callback url as the one in Auth0 settings:
const uri='http://localhost:3000/upload';
<Auth0Provider
domain={domain}
clientId={clientId}
redirectUri={uri}>
To cause a redirect to a different URL after a successful authentication, you need to provide the redirectUrl to Lock, like this:
// Configure Auth0
const options = {
allowedConnections: ['linkedin'],
auth: {
responseType: 'code',
redirectUrl: 'http://localhost:3000/upload'
}
};
this.lock = new Auth0Lock(clientId, domain, options)
(Also notice that the responseType option goes under auth, not under auth.params.)
If you do the redirect, you won't reach the event handler you defined in your login page. You will need to either add an event handler in your destination page (and use responseType:token) or handle authentication results in your server code (this is what you will normally be doing if you are requesting a responseType: code).
the reason why you should set the callback Url in auth0 settings, because any one can use your client id and send request to google or linkedin, get the response to anywhere they set. but with this setting only you can access that response.
once your app is authorized to pull the data from linkedin, linkedin will send the data to where you specified. you should create a page to handle the response from Linkedin server. Let's name that page callback.js and this will be an example of response object.
accessToken: "hNuPLKTZHiE9_lnED0JIiiPNjlicRDp"
appState: null
expiresIn: 7200
idToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRXdSVUl5TURVeE4wSkJPRFZEUlRKRU1EVkZNemsxTXpNNU5VTXlNRGt6T0VWQlJqUkZRUSJ9.eyJodHRwOi8vbG9jYWxob3N0OjMwMDAvcm9sZSI6InNpdGVPd25lciIsImdpdmVuX25hbWUiOiJvbWFyIiwiZmFtaWx5X25hbWUiOiJpYm8iLCJuaWNrbmFtZSI6Im9tYXJpYm8xOTgyIiwibmFtZSI6Im9tYXIgaWJvIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250BQUFBQUkvQUFBQUFBQUFBQUEvQUNIaTNyLTEwLTEyVDIyOjU4OjAxLjgzM1oiLCJpc3MiOiJodHRwczovL3BvcnRmb2xpby15aWxtYXouYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTE0MDY0NTA2ODI2OTgwNTA5ODY3IiwiYXVkIjoiUEdVY242RjRRS21PRkJhb1k0UFdCeWpjVzIyT09vNGMiLCJpYXQiOjE1NzA5MjEwODIsImV4cCI6MTU3MDk1NzA4MiwiYXRfaGFzaCI6InN0R1l5SnJaMHNnbVYzSWNLWjlPeFEiLCJub25jZSI6InRrOV95b096enRmVThVVjFVMlVFR3IyMW5ORW5abjk4In0.TYS7mM8N2d7jEHFdWQGTSeAAUaDt4-0SMUG3LrcQ1r3xzY0RMGsUsEszj5xqk1GE0cIlFS10xCOYKsuHSwsFLomC1EbLjntjkledHtfD0MW84cMoXN6a-x-1-bNwl3lMYJ98qklTrNvTvkQJ6DWhei3hJ8rs8dnbNyCfckNVU6ptJU-9ef1DwWfHRomW5LQ6WSDRHZScW697gdgBEMU-Nd2SddyHhQe0kVh6lKdcbnskEAyCJLE07jfM40RQI_8LJouFcpoyImcXSDZlKv90tYfVDq9_TwE3GNaSz5I5snn0457oCgz0vuX0JoCUiaDuTIX7XiyXnozW_DxGMuhk4w"
idTokenPayload: {http://localhost:3000/role: "siteOwner", given_name: "me", family_name: "you", nickname: "nck", name: "nm", …}
refreshToken: null
scope: null
state: "xkEbffzXbdOYPLkXOUkrQeb0Jysbnlfy"
tokenType: "Bearer"
//THIS CODE IS FOR NEXT.JS9
//auth.js
class Auth0 {
constructor() {
this.auth0 = new auth0.WebAuth({
domain: "portfolio-ys.auth0.com",
clientID: "PGUWJQKmOFBaoY4PWByjcW22OOo4c",
redirectUri: "http://localhost:3000/callback",
responseType: "token id_token",
scope: "openid profile"
});
this.handleAuthentication = this.handleAuthentication.bind(this);
}
//there are too many methods are defined here i put only relevant ones
handleAuthentication() {
return new Promise((resolve, reject) => {
this.auth0.parseHash((err, authResult) => {
console.log(authResult);
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult);
resolve();
} else if (err) {
reject(err);
}
});
});
}
setSession function is where you set the cookies based on response object. I use js-cookie package to set the cookie.
setSession(authResult) {
const expiresAt = JSON.stringify(
authResult.expiresIn * 1000 + new Date().getTime()
);
Cookies.set("user", authResult.idTokenPayload);
Cookies.set("jwt", authResult.idToken);
Cookies.set("expiresAt", expiresAt);
}
}
const auth0Client = new Auth0();
export default auth0Client;
callback.js
import React from "react"
import auth0Client from "./auth0"
import {withRouter} from "next/router"
class Callback extends React.Component{
async componentDidMount(){
await auth0Client.handleAuthentication()
this.props.router.push('/')
}
render() {
return (
<h1>verifying logging data</h1>
)
}
}
export default withRouter(Callback) //this allows us to use router
I had similar issue "callback URL mismatch" and resolved it by running the application over https with a trusted certificate.
Here is a snippet from Auth0 applications settings section about callback URL, which says "Make sure to specify the protocol (https://) otherwisw the callback may fail in some cases."
If you're using the Android(Kotlin) SDK of auth0, I noticed that during runtime, the requested URL is being changed. e.g. app://{your_auth0_domain}/android/{package_name}/callback://{your_auth0_domain}/android/app://{your_auth0_domain}/android//callback
Originally URL was
app://{your_auth0_domain}/android/{package_name}/callback
and SDK is appending "://{your_auth0_domain}/android/app://{your_auth0_domain}/android//callback" this extra part.
Solution: Either put the same URL in auth0 setting dashboard as it showing in your logs
or
WebAuthProvider
.login(account)
.withScheme("app") // instead of complete URL, put only the remaining part from the URL,
.start(this, object : Callback<Credentials, AuthenticationException> {}
I hope it will definitely help android/app developer.

Programmatically closing a Durandal modal

I'm new to Durandal, so I might be taking the wrong approach to my problem.
I want to show a modal popup with a message of 'logging in...please wait' when the user has clicked the login button. I want to close the modal popup once the response is received. My attempted approach is to call a custom modal popup using Durandal's app.showModal and a view with no buttons, from the login view model. This shows the modal popup I'm after, but I haven't been able to figure out how to close the popup once the server response is received. All the examples I've seen have a button on the modal popup view that closes the popup.
Is this possible? If not, is there a better approach that will show the user something is happening and also stop the user from trying to use any other button on the view?
Here's the view model code for the login view (with extraneous code removed):
define(['services/appsecurity', 'durandal/plugins/router', 'services/utils', 'services/errorhandler', 'durandal/app', 'viewmodels/controls/activityindicator'],
function (appsecurity, router, utils, errorhandler, app, activityIndicator) {
var username = ko.observable().extend({ required: true }),
password = ko.observable().extend({ required: true, minLength: 6 }),
rememberMe = ko.observable(),
returnUrl = ko.observable(),
isRedirect = ko.observable(false),
var viewmodel = {
username: username,
password: password,
rememberMe: rememberMe,
returnUrl: returnUrl,
isRedirect: isRedirect,
appsecurity: appsecurity,
login: function() {
var credential = new appsecurity.credential(this.username(), this.password(), this.rememberMe() || false),
self = this;
activityIndicator.message = 'Logging in...please wait';
app.showModal(activityIndicator);
appsecurity.login(credential, self.returnUrl())
.fail(self.handlevalidationerrors)
.always(function() { activityIndicator.close(); });
}};
return viewmodel;
});
The appsecurity.login function is the ajax post call. The view model for the custom modal is:
define(function () {
var activityIndicator = function (message, title, options) {
this.message = message;
this.title = title || activityIndicator.defaultTitle;
this.options = options || activityIndicator.defaultOptions;
this.close = function() {
this.modal.close();
};
};
return activityIndicator;
});
When running this, I get an error on .always(function() { activityIndicator.close(); }); of close is undefined.
Note for anyone using Durandal 2.0, the above marked answer only works in earlier 1.x versions. Happy coding!
Found the problem. The viewmodel for the custom modal was wrong, so the close() method was undefined. The working viewmodel is:
define(function () {
var message = ko.observable();
var activityIndicator = {
message: message,
close: function() {
this.modal.close();
}
};
return activityIndicator;
});