Authentication
alt-seven authentication tutorial
Configuration
In order to enable built-in authentication, you must configure your application with the correct options. Here is a sample of the authentication options to be passed to the framework:
remote: {
modules: app.remote,
loginURL: "/api/auth/login",
logoutURL: "/api/auth/logout",
refreshURL: "api/auth/refresh"
},
In addition to the authentication methods, the a7 model must be enabled for authentication to work. See the page on configuring your application to understand all the options available to you.
Built-in Events and Remote Methods
The event system contains a set of pre-defined events and corresponding remote methods for the built-in authentication system. For login, you need to pass a params object to the event in this format { username: "", password: "", success: ""|func, failure: ""|func }
.
a7.events.subscribe("auth.login", function(params) {
a7.remote.invoke("auth.login", params);
});
a7.events.subscribe("auth.logout", function(params) {
a7.remote.invoke("auth.logout", params);
});
a7.events.subscribe("auth.refresh", function(params) {
a7.remote.invoke("auth.refresh", params);
});
a7.events.subscribe("auth.sessionTimeout", function() {
a7.security.invalidateSession();
});
a7.events.subscribe("auth.invalidateSession", function() {
a7.security.invalidateSession();
});
Calling the login event
In the login form, the programmer can then specify success and failure parameters, which can be methods, event names, or router destinations. In the below example, you send the username and password to the auth.login event and specify success and failure methods to run after the authentication method returns from the server.
a7.events.publish('auth.login', {
username: loginform.state.username,
password: loginform.state.password,
success: function( json ){
main.runApp();
},
failure: function( json ){
state.message = json.message;
state.username = "";
state.password = "";
loginform.setState( state );
loginform.fireEvent("mustRender");
}
});
Options for success and failure params
Success and failure could also be specified as events, such as:
success: "auth.loginSuccess",
failure: "auth.loginFailure"
or as router destinations:
success: "auth/loginSuccess",
failure: "auth/loginFailure"
The programmer must then create the events or router destinations named:
a7.events.subscribe( "auth.loginSuccess", function( obj ){
// handle login success tasks
});
You can see more details on working with the router in the routing tutorial.
Creating remote authentication methods
You can build remote methods for authentication on the platform of your choice. The remote methods must satisfy these contracts:
The User object
Alt-seven has a built-in User object that it employs for its authentication system. The User object by default is an empty object that is populated with the keys returned in the login response from the remote method. In order to facilitate authentication checking, you can specify a set of default keys, such as userID, to populate the anonymous User object when a system user is not authenticated. When the user is authenticated, the login response sets the user into the a7 model and into sessionStorage.user. The logout response clears the existing user and sets an anonymous user into the model and sessionStorage.user.
// the User object may be extended in the future with events you can listen to, so this would be the preferred method for creating a user object
let user = a7.components.Constructor(
a7.components.User,
[{ userID: '' }],
true // add events
)
// or simply
let user = a7.components.User( { userID: '' } ); // no events will be added to the user object
login
Input arguments: String username String password
Return:
Note that the login remote method uses Basic Authentication, sending the username and password as Base64-encoded strings in the method header. The server authentication method must check the Authorization header to get the username and password. See the code from a7.remote below:
login: function( params ){
a7.log.trace( "remote call: auth.login" );
var request,
args = { method: 'POST',
headers: {
"Authorization": "Basic " + a7.util.base64.encode64( params.username + ":" + params.password )
}
};
request = new Request( _options.loginURL , args );
var promise = fetch( request );
// continues ...
The returned user object, in JSON format, will be parsed by the response in the remote method, the keys from the user object will be added to the built-in alt-seven User object, and the user object will be set in the model as so a7.model.set( "user", user )
.
If you are using tokens for the remote methods, the response will look for the ‘X-Token’ response header and set the returned token into sessionStorage.token. This token will be used to keep the user’s session alive using the refresh() remote method.
Let’s look at the remote authentication method in the LacesIDE open source application. It provides a good example of how to work with the auth meachanism in NodeJS. Other languages and platforms will vary. The controller methods for login, logout, and refresh are listed below. They contain the heart of the functionality you need to understand. You can look through the LacesIDE source code if you want to see the full details of the implementation. Start with routes/api.auth.js
and go from there.
The remote method extracts the username and password from the Authorization header, Base64 decodes the authorization string, then splits it into the username and password components. In this example, userservice.getByUsername(username) retrieves the user record from the database, the password is hashed with crypto.pbkdf2Sync, and the hash is compared to the password hash stored in the databaase. Note that the hash and salt are removed from the user record for security before the user record is returned to the client. LacesIDE uses tokens, so the X-Token is set with the value of a generated token, and the response is encoded and returned.
login: function (request, response) {
let authorization = request.header("Authorization") || "";
let username = "";
let password = "";
if (authorization.length) {
let authArray = authorization.split(" ");
authorization = Base64.decode(authArray[1]);
username = authorization.split(":")[0];
password = authorization.split(":")[1];
}
userservice.getByUsername(username)
.then(function (results) {
let valid = false;
if (results) {
let hash = crypto.pbkdf2Sync(password, results.salt, 1000, 64, `sha512`).toString(`hex`);
valid = ( hash === results.hash );
}
if (valid) {
let user = results;
// remove the hash and salt from the token so we don't send it outside the system
delete user.hash;
delete user.salt;
response.setHeader("X-Token", utils.generateToken(user));
response.send({ user: user, success: true });
} else {
throw ({ success: false, error: "Invalid username/password combination." });
}
})
.catch(function (error) {
console.log(error);
response.send(JSON.stringify(error));
});
},
logout: function (request, response) {
response.send({ success: true });
},
refresh: function (request, response) {
//let token = request.header( "X-Token" );
response.send({ success: true });
}
Session Tokens
Alt-seven uses the session tokens to authenticate a user during the logged in session. Note that the timeout of the server side session should match the timeout of the timeout value in the alt-seven application so the session stays active as long as the user is logged in and the browser window stays open. This is the default behavior of the application.
While the user is authenticated, alt-seven will automatically include the X-Token header in every remote request, and will pass the current token for the user in that header. The token should be in string format in order for the system to pass it back and forth in the X-Token header. Beyond that, the format and contents of the token are left to the programmer. Below are the generateToken and checkAuthToken methods from the LacesIDE application. In this case, you can see that the token is the user object with a key expires
added to it. The expires
key is used to check the expiration of the session.
generateToken: function( user ){
const hash = new SHA3(512);
let authtoken = Object.assign( {}, user );
let now = new Date();
authtoken.expires = new Date( new Date( now ).getTime() + securityConfig.ttl );
console.log( "current date " + new Date( now ) );
console.log( "expires " + authtoken.expires );
let base64Token = Base64.encode( JSON.stringify( authtoken ) );
//let base64Token = Base64.encode( authtoken );
hash.update( base64Token );
let myhash = hash.digest( 'hex' );
console.log( myhash );
return JSON.stringify( { token: base64Token, hash: myhash } );
},
checkAuthToken: function( authtoken, timeout ){
const hash = new SHA3(512);
let auth;
authtoken = JSON.parse( authtoken );
let token = authtoken.token;
let hashCode = authtoken.hash;
hash.update( token );
let myhash = hash.digest( 'hex' );
if( hashCode == myhash ){
// token is valid
auth = JSON.parse( Base64.decode( token ) );
}
return auth;
},
LacesIDE uses the Express framework, which enables the user of interceptors to check HTTP inbound requests before they are routed to the rest of the application. The application combines an httpinterceptor with route-based security to determine wheter a user is allowed to access a given URL path in the application. Below is the checkHTTPAuth method from the interceptor, used to authenticate the user with each request. If you use the alt-seven authentication system with tokens, how you implement session management on the server is up to you. This code from LacesIDE provides one method of implementing session management in NodeJS.
checkHTTPAuth: function (request, response, next) {
// is it an open route?
let openRoute = securityConfig.routes.open.find(function (route) {
//console.log( "route: " + route );
return request.url.match(route);
});
// is it a secured route?
let securedRoute = securityConfig.routes.secured.find(function (route) {
//console.log( "route: " + route );
return request.url.match(route);
});
console.log("Checking auth");
// check token
let token = request.headers["x-token"];
// anonymous access
if (token === undefined || token.length === 0) {
// not a logged in user
if (openRoute !== undefined) {
// if this is an open route, pass them along
next();
} else if (securedRoute !== undefined) {
notAuthorized(request, response);
} else {
// route not found
console.log("WARN: Route not found for URL:" + request.url);
console.log("Pass through allowed.");
next();
}
} else {
let auth = utils.checkAuthToken(token, securityConfig.ttl);
userservice.read(auth.userID)
.then(function (results) {
let valid = false;
let user = results;
let now = new Date();
console.log("userID: " + user.userID);
let expires = new Date(auth.expires).getTime() ;
let nowtime = now.getTime();
console.log( "sessiontime: " + expires - nowtime );
// if there is a valid logged in user, pass them
if (user.userID.length > 0 && (new Date(auth.expires).getTime() - now.getTime() > 0)) {
valid = true;
// remove the password hash from the token so we don't send it outside the system
delete user.hash;
delete user.salt;
// this call sets a user into the request
request.user = user;
// set a new header token
response.setHeader("X-Token", utils.generateToken(user));
console.log("X-Token set");
// move on to the route handlers
//next();
} else if (user.userID.length > 0) {
console.log("Expires: " + new Date(auth.expires).getTime());
console.log("Now: " + now.getTime());
console.log("Authorization expired.");
response.messages = "Authorization expired.";
}
if (securedRoute !== undefined) {
console.log(request.url + " matches " + securedRoute);
console.log("User validated? " + valid);
if (valid === true) {
// securedRoute and logged in user, forward them along
next();
} else {
notAuthorized(request, response);
}
} else {
if (openRoute === undefined) {
console.log("WARN: Route not found for URL:" + request.url);
console.log("Pass through allowed.");
}
// open routes pass through
next();
}
})
.catch(function (error) {
console.log(error);
response.status(500);
response.send("Error");
});
}
}
Logout
Logout, like login, is handled automatically by the alt-seven framework. Simply call the auth.login event and pass the success and failure arguments, which, like the login arguments, can be functions, events, or router paths.
a7.events.publish('auth.logout', {
success: "/auth/logoutsuccess",
failure: "/auth/logoutfailure"
});
Note that the auth.logout method sets the Authorization header with the username and password from the event arguments, like the login method, but these are optional arguments. You can include those arguments if you have the username and password cached on the client and you want to use it on the server side.
a7.events.publish('auth.logout', {
username: username,
password: password,
success: "/auth/logoutsuccess",
failure: "/auth/logoutfailure"
});
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.