diff --git a/app/scripts/directives/oauth.js b/app/scripts/directives/oauth.js
index a5d9690..91fd5d7 100644
--- a/app/scripts/directives/oauth.js
+++ b/app/scripts/directives/oauth.js
@@ -21,25 +21,27 @@ directives.directive('oauth', [
restrict: 'AE',
replace: true,
scope: {
- site: '@', // (required) set the oauth server host (e.g. http://oauth.example.com)
- clientId: '@', // (required) client id
- redirectUri: '@', // (required) client redirect uri
- responseType: '@', // (optional) response type, defaults to token (use 'token' for implicit flow and 'code' for authorization code flow
- scope: '@', // (optional) scope
- profileUri: '@', // (optional) user profile uri (e.g http://example.com/me)
- template: '@', // (optional) template to render (e.g views/templates/default.html)
- text: '@', // (optional) login text
- authorizePath: '@', // (optional) authorization url
- state: '@', // (optional) An arbitrary unique string created by your app to guard against Cross-site Request Forgery
- storage: '@', // (optional) Store token in 'sessionStorage' or 'localStorage', defaults to 'sessionStorage'
- nonce: '@', // (optional) Send nonce on auth request
- // OpenID Connect extras, more details in id-token.js:
- issuer: '@', // (optional for OpenID Connect) issuer of the id_token, should match the 'iss' claim in id_token payload
- subject: '@', // (optional for OpenID Connect) subject of the id_token, should match the 'sub' claim in id_token payload
- pubKey: '@', // (optional for OpenID Connect) the public key(RSA public key or X509 certificate in PEM format) to verify the signature
- wellKnown: '@', // (optional for OpenID Connect) whether to load public key according to .well-known/openid-configuration endpoint
- logoutPath: '@', // (optional) A url to go to at logout
- sessionPath: '@' // (optional) A url to use to check the validity of the current token.
+ site: '@', // (required) set the oauth server host (e.g. http://oauth.example.com)
+ clientId: '@', // (required) client id
+ redirectUri: '@', // (required) client redirect uri
+ responseType: '@', // (optional) response type, defaults to token (use 'token' for implicit flow, 'code' for authorization code flow and 'password' for resource owner password
+ scope: '@', // (optional) scope
+ profileUri: '@', // (optional) user profile uri (e.g http://example.com/me)
+ template: '@', // (optional) template to render (e.g views/templates/default.html)
+ text: '@', // (optional) login text
+ authorizePath: '@', // (optional) authorization url
+ tokenPath: '@', // (optional) token url
+ state: '@', // (optional) An arbitrary unique string created by your app to guard against Cross-site Request Forgery
+ storage: '@', // (optional) Store token in 'sessionStorage' or 'localStorage', defaults to 'sessionStorage'
+ nonce: '@', // (optional) Send nonce on auth request
+ // OpenID Connect extras, more details in id-token.js:
+ issuer: '@', // (optional for OpenID Connect) issuer of the id_token, should match the 'iss' claim in id_token payload
+ subject: '@', // (optional for OpenID Connect) subject of the id_token, should match the 'sub' claim in id_token payload
+ pubKey: '@', // (optional for OpenID Connect) the public key(RSA public key or X509 certificate in PEM format) to verify the signature
+ wellKnown: '@', // (optional for OpenID Connect) whether to load public key according to .well-known/openid-configuration endpoint
+ logoutPath: '@', // (optional) A url to go to at logout
+ sessionPath: '@', // (optional) A url to use to check the validity of the current token.
+ disableCheckSession:'@' // (optional) can current token be checked ?
}
};
@@ -58,22 +60,29 @@ directives.directive('oauth', [
OidcConfig.load(scope) // loads OIDC configuration from .well-known/openid-configuration if necessary
.then(function() {
IdToken.set(scope);
- AccessToken.set(scope); // sets the access token object (if existing, from fragment or session)
- initProfile(scope); // gets the profile resource (if existing the access token)
- initView(); // sets the view (logged in or out)
- checkValidity(); // ensure the validity of the current token
+ AccessToken.set(scope).then(function () { // sets the access token object (if existing, from fragment or session)
+ })
+ ["finally"](function () {
+ initProfile(scope); // gets the profile resource (if existing the access token)
+ initView(); // sets the view (logged in or out)
+ checkValidity(); // ensure the validity of the current token
+ });
});
};
var initAttributes = function() {
- scope.authorizePath = scope.authorizePath || '/oauth/authorize';
- scope.tokenPath = scope.tokenPath || '/oauth/token';
- scope.template = scope.template || 'views/templates/default.html';
- scope.responseType = scope.responseType || 'token';
- scope.text = scope.text || 'Sign In';
- scope.state = scope.state || undefined;
- scope.scope = scope.scope || undefined;
- scope.storage = scope.storage || 'sessionStorage';
+ scope.authorizePath = scope.authorizePath || '/oauth/authorize';
+ scope.tokenPath = scope.tokenPath || '/oauth/token';
+ scope.template = scope.template || 'views/templates/default.html';
+ scope.responseType = scope.responseType || 'token';
+ scope.text = scope.text || 'Sign In';
+ scope.state = scope.state || undefined;
+ scope.scope = scope.scope || undefined;
+ scope.storage = scope.storage || 'sessionStorage';
+ scope.disableCheckSession = scope.disableCheckSession || false;
+ scope.typedLogin = "";
+ scope.typedPassword = "";
+ scope.typedKeepConnection = false;
};
var compile = function() {
@@ -96,8 +105,8 @@ directives.directive('oauth', [
var initView = function () {
var token = AccessToken.get();
- if (!token) {
- return scope.login();
+ if (!token && scope.responseType !== "password") {
+ return expired();
} // without access token it's logged out, so we attempt to log in
if (AccessToken.expired()) {
return expired();
@@ -115,12 +124,43 @@ directives.directive('oauth', [
};
scope.logout = function () {
+ scope.typedLogin = "";
+ scope.typedPassword = "";
+ scope.typedKeepConnection = false;
Endpoint.logout();
$rootScope.$broadcast('oauth:loggedOut');
scope.show = 'logged-out';
+ AccessToken.destroy();
+ };
+
+ scope.checkPassword = function () {
+ $http({
+ method: "POST",
+ url: scope.site + scope.tokenPath,
+ headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+ transformRequest: function(obj) {
+ var str = [];
+ for(var p in obj)
+ str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
+ return str.join("&");
+ },
+ data: {grant_type: "password", username: scope.typedLogin, password: scope.typedPassword, scope: scope.scope}
+ }).then(function (result) {
+ if (scope.typedKeepConnection) {
+ AccessToken.setTokenFromPassword(scope, result.data, scope.typedLogin, scope.typedPassword, scope.scope);
+ } else {
+ AccessToken.setTokenFromPassword(scope, result.data);
+ scope.typedLogin = "";
+ scope.typedPassword = "";
+ scope.typedKeepConnection = false;
+ }
+ scope.show = "logged-in";
+ }, function () {
+ $rootScope.$broadcast('oauth:denied');
+ });
};
- scope.$on('oauth:expired',expired);
+ scope.$on('oauth:expired', expired);
// user is authorized
var authorized = function() {
@@ -129,9 +169,13 @@ directives.directive('oauth', [
};
var expired = function() {
- $rootScope.$broadcast('oauth:expired');
+ scope.show = 'logged-out';
scope.logout();
};
+
+ scope.runExpired = function() {
+ expired();
+ };
// set the oauth directive to the denied status
var denied = function() {
@@ -165,6 +209,7 @@ directives.directive('oauth', [
scope.$on('$stateChangeSuccess', function () {
$timeout(refreshDirective);
});
+
};
return definition;
diff --git a/app/scripts/services/access-token.js b/app/scripts/services/access-token.js
index 7e34d9b..f5365ab 100644
--- a/app/scripts/services/access-token.js
+++ b/app/scripts/services/access-token.js
@@ -2,203 +2,373 @@
var accessTokenService = angular.module('oauth.accessToken', []);
-accessTokenService.factory('AccessToken', ['Storage', '$rootScope', '$location', '$interval', '$timeout', 'IdToken', function(Storage, $rootScope, $location, $interval, $timeout, IdToken){
-
- var service = {
- token: null
- },
- hashFragmentKeys = [
- //Oauth2 keys per http://tools.ietf.org/html/rfc6749#section-4.2.2
- 'access_token', 'token_type', 'expires_in', 'scope', 'state',
- 'error','error_description',
- //Additional OpenID Connect key per http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse
- 'id_token'
- ];
- var expiresAtEvent = null;
-
- /**
- * Returns the access token.
- */
- service.get = function(){
- return this.token;
- };
-
- /**
- * Sets and returns the access token. It tries (in order) the following strategies:
- * - takes the token from the fragment URI
- * - takes the token from the sessionStorage
- */
- service.set = function(){
- this.setTokenFromString($location.hash());
-
- //If hash is present in URL always use it, cuz its coming from oAuth2 provider redirect
- if(null === service.token){
- setTokenFromSession();
- }
+accessTokenService.factory('AccessToken', ['Storage', '$rootScope', '$http', '$q', '$location', '$interval', '$timeout', 'IdToken', function(Storage, $rootScope, $http, $q, $location, $interval, $timeout, IdToken) {
- return this.token;
- };
-
- /**
- * Delete the access token and remove the session.
- * @returns {null}
- */
- service.destroy = function(){
- Storage.delete('token');
- this.token = null;
- return this.token;
- };
-
- /**
- * Tells if the access token is expired.
- */
- service.expired = function(){
- return (this.token && this.token.expires_at && new Date(this.token.expires_at) < new Date());
- };
-
- /**
- * Get the access token from a string and save it
- * @param hash
- */
- service.setTokenFromString = function(hash){
- var params = getTokenFromString(hash);
-
- if(params){
- removeFragment();
- setToken(params);
- setExpiresAt();
- // We have to save it again to make sure expires_at is set
- // and the expiry event is set up properly
- setToken(this.token);
- $rootScope.$broadcast('oauth:login', service.token);
- }
- };
-
- /**
- * updates the expiration of the token
- */
- service.updateExpiry = function(newExpiresIn){
- this.token.expires_in = newExpiresIn;
- setExpiresAt();
- };
-
- /* * * * * * * * * *
- * PRIVATE METHODS *
- * * * * * * * * * */
-
- /**
- * Set the access token from the sessionStorage.
- */
- var setTokenFromSession = function(){
- var params = Storage.get('token');
- if (params) {
- setToken(params);
- }
- };
-
- /**
- * Set the access token.
- *
- * @param params
- * @returns {*|{}}
- */
- var setToken = function(params){
- service.token = service.token || {}; // init the token
- angular.extend(service.token, params); // set the access token params
- setTokenInSession(); // save the token into the session
- setExpiresAtEvent(); // event to fire when the token expires
-
- return service.token;
- };
-
- /**
- * Parse the fragment URI and return an object
- * @param hash
- * @returns {{}}
- */
- var getTokenFromString = function(hash){
- var params = {},
- regex = /([^&=]+)=([^&]*)/g,
- m;
-
- while ((m = regex.exec(hash)) !== null) {
- params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
- }
+ var service = {
+ token: null,
+ typedLogin: "",
+ typedPassword: "",
+ scope: "",
+ runExpired: null
+ },
+ hashFragmentKeys = [
+ //Oauth2 keys per http://tools.ietf.org/html/rfc6749#section-4.2.2
+ 'access_token', 'token_type', 'expires_in', 'scope', 'state',
+ 'error', 'error_description',
+ //Additional OpenID Connect key per http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse
+ 'id_token'
+ ];
+ var expiresAtEvent = null;
+ var refreshTokenUri = null;
- // OpenID Connect
- if (params.id_token && !params.error) {
- IdToken.validateTokensAndPopulateClaims(params);
- return params;
- }
+ /**
+ * Returns the access token.
+ */
+ service.get = function() {
+ return this.token;
+ };
- // Oauth2
- if(params.access_token || params.error){
- return params;
- }
- };
-
- /**
- * Save the access token into the session
- */
- var setTokenInSession = function(){
- Storage.set('token', service.token);
- };
-
- /**
- * Set the access token expiration date (useful for refresh logics)
- */
- var setExpiresAt = function(){
- if (!service.token) {
- return;
- }
- if(typeof(service.token.expires_in) !== 'undefined' && service.token.expires_in !== null) {
- var expires_at = new Date();
- expires_at.setSeconds(expires_at.getSeconds() + parseInt(service.token.expires_in)-60); // 60 seconds less to secure browser and response latency
- service.token.expires_at = expires_at;
- }
- else {
- service.token.expires_at = null;
- }
- };
+ /**
+ * Sets and returns the access token. It tries (in order) the following strategies:
+ * - Get the token using the code in the url
+ * - takes the token from the fragment URI
+ * - takes the token from the sessionStorage
+ */
+ service.set = function(scope) {
+ refreshTokenUri = scope.site + scope.tokenPath;
+ this.runExpired = scope.runExpired;
+ if ($location.search().code) {
+ return this.setTokenFromCode($location.search(), scope);
+ }
- /**
- * Set the timeout at which the expired event is fired
- */
- var setExpiresAtEvent = function(){
- // Don't bother if there's no expires token
- if (typeof(service.token.expires_at) === 'undefined' || service.token.expires_at === null) {
- return;
- }
- cancelExpiresAtEvent();
- var time = (new Date(service.token.expires_at))-(new Date());
- if(time && time > 0 && time <= 2147483647){
- expiresAtEvent = $interval(function(){
- $rootScope.$broadcast('oauth:expired', service.token);
- }, time, 1);
+ this.setTokenFromString($location.hash());
+
+ //If hash is present in URL always use it, cuz its coming from oAuth2 provider redirect
+
+ var deferred = $q.defer();
+
+ if (this.token) {
+ deferred.resolve(this.token);
+ } else {
+ deferred.reject();
+ }
+
+ if (null === service.token) {
+ return setTokenFromSession();
+ } else {
+ return deferred.promise;
+ }
+ };
+
+ service.setTokenFromPassword = function(scope, token, typedLogin, typedPassword, oauthScope) {
+ this.runExpired = scope.runExpired;
+ if (typedLogin && typedPassword && oauthScope) {
+ service.typedLogin = typedLogin;
+ service.typedPassword = typedPassword;
+ service.scope = oauthScope;
+ }
+ setToken(token);
+ $rootScope.$broadcast('oauth:login', token);
}
- };
- var cancelExpiresAtEvent = function() {
- if(expiresAtEvent) {
- $timeout.cancel(expiresAtEvent);
- expiresAtEvent = undefined;
+ /**
+ * Delete the access token and remove the session.
+ * @returns {null}
+ */
+ service.destroy = function() {
+ cancelExpiresAtEvent();
+ Storage.delete('token');
+ this.token = null;
+ return this.token;
+ };
+
+ /**
+ * Tells if the access token is expired.
+ */
+ service.expired = function() {
+ return (this.token && this.token.expires_at && new Date(this.token.expires_at) < new Date());
+ };
+
+ service.setTokenFromCode = function(search, scope) {
+ return $http({
+ method: "POST",
+ url: scope.site + scope.tokenPath,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ transformRequest: function(obj) {
+ var str = [];
+ for (var p in obj)
+ str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
+ return str.join("&");
+ },
+ data: {
+ grant_type: "authorization_code",
+ code: search.code,
+ redirect_uri: scope.redirectUri,
+ client_id: scope.clientId
+ }
+ }).then(function(result) {
+ setToken(result.data);
+ $rootScope.$broadcast('oauth:login', service.token);
+ $location.url(http://github.com/$location.path());
+ });
}
- };
-
- /**
- * Remove the oAuth2 pieces from the hash fragment
- */
- var removeFragment = function(){
- var curHash = $location.hash();
- angular.forEach(hashFragmentKeys,function(hashKey){
- var re = new RegExp('&'+hashKey+'(=[^&]*)?|^'+hashKey+'(=[^&]*)?&?');
- curHash = curHash.replace(re,'');
- });
-
- $location.hash(curHash);
- };
-
- return service;
+
+ /**
+ * Get the access token from a string and save it
+ * @param hash
+ */
+ service.setTokenFromString = function(hash) {
+ var params = getTokenFromString(hash);
+
+ if (params) {
+ removeFragment();
+ setToken(params);
+ // We have to save it again to make sure expires_at is set
+ // and the expiry event is set up properly
+ setToken(this.token);
+ $rootScope.$broadcast('oauth:login', service.token);
+ }
+ };
+
+ /**
+ * updates the expiration of the token
+ */
+ service.updateExpiry = function(newExpiresIn) {
+ this.token.expires_in = newExpiresIn;
+ setExpiresAt();
+ };
+
+ service.forceRefresh = function(connect) {
+ return refreshToken(connect);
+ };
+
+ /* * * * * * * * * *
+ * PRIVATE METHODS *
+ * * * * * * * * * */
+
+ /**
+ * Set the access token from the sessionStorage.
+ */
+ var setTokenFromSession = function() {
+ var params = Storage.get('token');
+ if (params) {
+ setToken(params);
+ if (!params.refresh_token) {
+ var deferred = $q.defer();
+ deferred.resolve(params);
+ $rootScope.$broadcast('oauth:login', params);
+ return deferred.promise;
+ } else {
+ return refreshToken(true);
+ }
+ } else {
+ var deferred = $q.defer();
+ deferred.reject();
+ return deferred.promise;
+ }
+ };
+
+ var refreshToken = function(connect) {
+ if (service.token && service.token.refresh_token) {
+ return $http({
+ method: "POST",
+ url: refreshTokenUri,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ transformRequest: function(obj) {
+ var str = [];
+ for (var p in obj)
+ str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
+ return str.join("&");
+ },
+ data: {
+ grant_type: "refresh_token",
+ refresh_token: service.token.refresh_token
+ }
+ }).then(function(result) {
+ angular.extend(service.token, result.data);
+ setExpiresAt();
+ setTokenInSession();
+ if (connect) {
+ $rootScope.$broadcast('oauth:login', service.token);
+ } else {
+ $rootScope.$broadcast('oauth:refresh', service.token);
+ }
+ return result.data;
+ }, function(error) {
+ if (!!service.typedLogin && !!service.typedPassword) {
+ return reconnect();
+ } else {
+ if (error.status === 401 || error.status === 400) {
+ cancelExpiresAtEvent();
+ Storage.delete('token');
+ $rootScope.$broadcast('oauth:expired');
+ service.runExpired();
+ }
+ }
+ });
+ } else {
+ var deferred = $q.defer();
+ deferred.reject();
+ return deferred.promise;
+ }
+ };
+
+ var reconnect = function() {
+ return $http({
+ method: "POST",
+ url: refreshTokenUri,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ transformRequest: function(obj) {
+ var str = [];
+ for (var p in obj)
+ str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
+ return str.join("&");
+ },
+ data: {
+ grant_type: "password",
+ username: service.typedLogin,
+ password: service.typedPassword,
+ scope: service.scope
+ }
+ }).then(function(result) {
+ angular.extend(service.token, result.data);
+ setTokenInSession();
+ $rootScope.$broadcast('oauth:refresh', service.token);
+ }, function(error) {
+ if (!!service.typedLogin && !!service.typedPassword) {
+ return reconnect();
+ } else {
+ if (error.status === 401 || error.status === 400) {
+ cancelExpiresAtEvent();
+ Storage.delete('token');
+ $rootScope.$broadcast('oauth:expired');
+ service.runExpired();
+ }
+ }
+ });
+ };
+
+ /**
+ * Set the access token.
+ *
+ * @param params
+ * @returns {*|{}}
+ */
+ var setToken = function(params) {
+ service.token = service.token || {}; // init the token
+ angular.extend(service.token, params); // set the access token params
+ setTokenInSession(); // save the token into the session
+ setExpiresAt();
+ setExpiresAtEvent(); // event to fire when the token expires
+
+ return service.token;
+ };
+
+ /**
+ * Parse the fragment URI and return an object
+ * @param hash
+ * @returns {{}}
+ */
+ var getTokenFromString = function(hash) {
+ var params = {},
+ regex = /([^&=]+)=([^&]*)/g,
+ m;
+
+ while ((m = regex.exec(hash)) !== null) {
+ params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
+ }
+
+ // OpenID Connect
+ if (params.id_token && !params.error) {
+ IdToken.validateTokensAndPopulateClaims(params);
+ return params;
+ }
+
+ // Oauth2
+ if (params.access_token || params.error) {
+ return params;
+ }
+ };
+
+ /**
+ * Save the access token into the session
+ */
+ var setTokenInSession = function() {
+ Storage.set('token', service.token);
+ };
+
+ /**
+ * Set the access token expiration date (useful for refresh logics)
+ */
+ var setExpiresAt = function() {
+ if (!service.token) {
+ return;
+ }
+ if (typeof(service.token.expires_in) !== 'undefined' && service.token.expires_in !== null) {
+ var expires_at = new Date();
+ expires_at.setSeconds(expires_at.getSeconds() + parseInt(service.token.expires_in) - 60); // 60 seconds less to secure browser and response latency
+ service.token.expires_at = expires_at;
+ } else {
+ service.token.expires_at = null;
+ }
+ };
+
+
+ /**
+ * Set the interval at which the expired event is fired
+ */
+ var setExpiresAtEvent = function() {
+ // Don't bother if there's no expires token
+ if (typeof(service.token.expires_at) === 'undefined' || service.token.expires_at === null) {
+ return;
+ }
+ cancelExpiresAtEvent();
+ var time = (new Date(service.token.expires_at)) - (new Date());
+ if (time && time > 0 && time <= 2147483647) {
+ if (service.token.refresh_token) {
+ expiresAtEvent = $interval(function() {
+ refreshToken();
+ }, time);
+ } else {
+ expiresAtEvent = $timeout(function() {
+ $rootScope.$broadcast('oauth:expired');
+ service.runExpired();
+ }, time, 1);
+ }
+ }
+ };
+
+ var cancelExpiresAtEvent = function() {
+ if (expiresAtEvent) {
+ if (service.token.refresh_token) {
+ $interval.cancel(expiresAtEvent);
+ } else {
+ $timeout.cancel(expiresAtEvent);
+ }
+ expiresAtEvent = undefined;
+ }
+ };
+
+ /**
+ * Remove the oAuth2 pieces from the hash fragment
+ */
+ var removeFragment = function() {
+ var curHash = $location.hash();
+ angular.forEach(hashFragmentKeys, function(hashKey) {
+ var re = new RegExp('&' + hashKey + '(=[^&]*)?|^' + hashKey + '(=[^&]*)?&?');
+ curHash = curHash.replace(re, '');
+ });
+
+ $location.hash(curHash);
+ };
+
+ return service;
}]);
diff --git a/app/scripts/services/endpoint.js b/app/scripts/services/endpoint.js
index 6db8df6..f92fa5a 100644
--- a/app/scripts/services/endpoint.js
+++ b/app/scripts/services/endpoint.js
@@ -67,7 +67,7 @@ endpointClient.factory('Endpoint', ['$rootScope', 'AccessToken', '$q', '$http',
*/
service.checkValidity = function() {
var params = service.config;
- if( params.sessionPath ) {
+ if( params.sessionPath && !params.disableCheckSession ) {
var token = AccessToken.get();
if( !token ) {
return $q.reject("No token configured");
@@ -82,6 +82,8 @@ endpointClient.factory('Endpoint', ['$rootScope', 'AccessToken', '$q', '$http',
return $q.reject("Server replied: token is invalid.");
}
});
+ } else if (params.disableCheckSession) {
+ return true;
} else {
return $q.reject("You must give a :session-path param in order to validate the token.")
}
diff --git a/app/views/templates/button.html b/app/views/templates/button.html
index ce7861a..f30d62d 100644
--- a/app/views/templates/button.html
+++ b/app/views/templates/button.html
@@ -1,5 +1,25 @@
- Login Button
- Logout {{profile.email}}
- Access denied.
+ Login Button
+ Logout {{profile.email}}
+ Access denied.
+
+
+
+
diff --git a/app/views/templates/default.html b/app/views/templates/default.html
index dbd71f2..96dbe03 100644
--- a/app/views/templates/default.html
+++ b/app/views/templates/default.html
@@ -1,5 +1,25 @@
- {{text}}
- Logout {{profile.email}}
- Access denied. Try again.
+ {{text}}
+ Logout {{profile.email}}
+ Access denied. Try again.
+
+
+
+
diff --git a/dist/oauth-ng.js b/dist/oauth-ng.js
index 785f8fa..6d3f630 100644
--- a/dist/oauth-ng.js
+++ b/dist/oauth-ng.js
@@ -1,4 +1,4 @@
-/* oauth-ng - v0.4.10 - 2016-05-25 */
+/* oauth-ng - v0.4.10 - 2018-06-30 */
'use strict';
@@ -366,204 +366,374 @@ idTokenService.factory('IdToken', ['Storage', function(Storage) {
var accessTokenService = angular.module('oauth.accessToken', []);
-accessTokenService.factory('AccessToken', ['Storage', '$rootScope', '$location', '$interval', '$timeout', 'IdToken', function(Storage, $rootScope, $location, $interval, $timeout, IdToken){
+accessTokenService.factory('AccessToken', ['Storage', '$rootScope', '$http', '$q', '$location', '$interval', '$timeout', 'IdToken', function(Storage, $rootScope, $http, $q, $location, $interval, $timeout, IdToken) {
+
+ var service = {
+ token: null,
+ typedLogin: "",
+ typedPassword: "",
+ scope: "",
+ runExpired: null
+ },
+ hashFragmentKeys = [
+ //Oauth2 keys per http://tools.ietf.org/html/rfc6749#section-4.2.2
+ 'access_token', 'token_type', 'expires_in', 'scope', 'state',
+ 'error', 'error_description',
+ //Additional OpenID Connect key per http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse
+ 'id_token'
+ ];
+ var expiresAtEvent = null;
+ var refreshTokenUri = null;
+
+ /**
+ * Returns the access token.
+ */
+ service.get = function() {
+ return this.token;
+ };
- var service = {
- token: null
- },
- hashFragmentKeys = [
- //Oauth2 keys per http://tools.ietf.org/html/rfc6749#section-4.2.2
- 'access_token', 'token_type', 'expires_in', 'scope', 'state',
- 'error','error_description',
- //Additional OpenID Connect key per http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse
- 'id_token'
- ];
- var expiresAtEvent = null;
+ /**
+ * Sets and returns the access token. It tries (in order) the following strategies:
+ * - Get the token using the code in the url
+ * - takes the token from the fragment URI
+ * - takes the token from the sessionStorage
+ */
+ service.set = function(scope) {
+ refreshTokenUri = scope.site + scope.tokenPath;
+ this.runExpired = scope.runExpired;
- /**
- * Returns the access token.
- */
- service.get = function(){
- return this.token;
- };
+ if ($location.search().code) {
+ return this.setTokenFromCode($location.search(), scope);
+ }
- /**
- * Sets and returns the access token. It tries (in order) the following strategies:
- * - takes the token from the fragment URI
- * - takes the token from the sessionStorage
- */
- service.set = function(){
- this.setTokenFromString($location.hash());
+ this.setTokenFromString($location.hash());
- //If hash is present in URL always use it, cuz its coming from oAuth2 provider redirect
- if(null === service.token){
- setTokenFromSession();
- }
+ //If hash is present in URL always use it, cuz its coming from oAuth2 provider redirect
- return this.token;
- };
+ var deferred = $q.defer();
- /**
- * Delete the access token and remove the session.
- * @returns {null}
- */
- service.destroy = function(){
- Storage.delete('token');
- this.token = null;
- return this.token;
- };
+ if (this.token) {
+ deferred.resolve(this.token);
+ } else {
+ deferred.reject();
+ }
- /**
- * Tells if the access token is expired.
- */
- service.expired = function(){
- return (this.token && this.token.expires_at && new Date(this.token.expires_at) < new Date());
- };
+ if (null === service.token) {
+ return setTokenFromSession();
+ } else {
+ return deferred.promise;
+ }
+ };
- /**
- * Get the access token from a string and save it
- * @param hash
- */
- service.setTokenFromString = function(hash){
- var params = getTokenFromString(hash);
-
- if(params){
- removeFragment();
- setToken(params);
- setExpiresAt();
- // We have to save it again to make sure expires_at is set
- // and the expiry event is set up properly
- setToken(this.token);
- $rootScope.$broadcast('oauth:login', service.token);
+ service.setTokenFromPassword = function(scope, token, typedLogin, typedPassword, oauthScope) {
+ this.runExpired = scope.runExpired;
+ if (typedLogin && typedPassword && oauthScope) {
+ service.typedLogin = typedLogin;
+ service.typedPassword = typedPassword;
+ service.scope = oauthScope;
+ }
+ setToken(token);
+ $rootScope.$broadcast('oauth:login', token);
}
- };
- /**
- * updates the expiration of the token
- */
- service.updateExpiry = function(newExpiresIn){
- this.token.expires_in = newExpiresIn;
- setExpiresAt();
- };
+ /**
+ * Delete the access token and remove the session.
+ * @returns {null}
+ */
+ service.destroy = function() {
+ cancelExpiresAtEvent();
+ Storage.delete('token');
+ this.token = null;
+ return this.token;
+ };
- /* * * * * * * * * *
- * PRIVATE METHODS *
- * * * * * * * * * */
+ /**
+ * Tells if the access token is expired.
+ */
+ service.expired = function() {
+ return (this.token && this.token.expires_at && new Date(this.token.expires_at) < new Date());
+ };
- /**
- * Set the access token from the sessionStorage.
- */
- var setTokenFromSession = function(){
- var params = Storage.get('token');
- if (params) {
- setToken(params);
+ service.setTokenFromCode = function(search, scope) {
+ return $http({
+ method: "POST",
+ url: scope.site + scope.tokenPath,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ transformRequest: function(obj) {
+ var str = [];
+ for (var p in obj)
+ str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
+ return str.join("&");
+ },
+ data: {
+ grant_type: "authorization_code",
+ code: search.code,
+ redirect_uri: scope.redirectUri,
+ client_id: scope.clientId
+ }
+ }).then(function(result) {
+ setToken(result.data);
+ $rootScope.$broadcast('oauth:login', service.token);
+ $location.url(http://github.com/$location.path());
+ });
}
- };
- /**
- * Set the access token.
- *
- * @param params
- * @returns {*|{}}
- */
- var setToken = function(params){
- service.token = service.token || {}; // init the token
- angular.extend(service.token, params); // set the access token params
- setTokenInSession(); // save the token into the session
- setExpiresAtEvent(); // event to fire when the token expires
+ /**
+ * Get the access token from a string and save it
+ * @param hash
+ */
+ service.setTokenFromString = function(hash) {
+ var params = getTokenFromString(hash);
+
+ if (params) {
+ removeFragment();
+ setToken(params);
+ // We have to save it again to make sure expires_at is set
+ // and the expiry event is set up properly
+ setToken(this.token);
+ $rootScope.$broadcast('oauth:login', service.token);
+ }
+ };
- return service.token;
- };
+ /**
+ * updates the expiration of the token
+ */
+ service.updateExpiry = function(newExpiresIn) {
+ this.token.expires_in = newExpiresIn;
+ setExpiresAt();
+ };
- /**
- * Parse the fragment URI and return an object
- * @param hash
- * @returns {{}}
- */
- var getTokenFromString = function(hash){
- var params = {},
- regex = /([^&=]+)=([^&]*)/g,
- m;
+ service.forceRefresh = function(connect) {
+ return refreshToken(connect);
+ };
- while ((m = regex.exec(hash)) !== null) {
- params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
- }
+ /* * * * * * * * * *
+ * PRIVATE METHODS *
+ * * * * * * * * * */
- // OpenID Connect
- if (params.id_token && !params.error) {
- IdToken.validateTokensAndPopulateClaims(params);
- return params;
- }
+ /**
+ * Set the access token from the sessionStorage.
+ */
+ var setTokenFromSession = function() {
+ var params = Storage.get('token');
+ if (params) {
+ setToken(params);
+ if (!params.refresh_token) {
+ var deferred = $q.defer();
+ deferred.resolve(params);
+ $rootScope.$broadcast('oauth:login', params);
+ return deferred.promise;
+ } else {
+ return refreshToken(true);
+ }
+ } else {
+ var deferred = $q.defer();
+ deferred.reject();
+ return deferred.promise;
+ }
+ };
- // Oauth2
- if(params.access_token || params.error){
- return params;
- }
- };
+ var refreshToken = function(connect) {
+ if (service.token && service.token.refresh_token) {
+ return $http({
+ method: "POST",
+ url: refreshTokenUri,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ transformRequest: function(obj) {
+ var str = [];
+ for (var p in obj)
+ str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
+ return str.join("&");
+ },
+ data: {
+ grant_type: "refresh_token",
+ refresh_token: service.token.refresh_token
+ }
+ }).then(function(result) {
+ angular.extend(service.token, result.data);
+ setExpiresAt();
+ setTokenInSession();
+ if (connect) {
+ $rootScope.$broadcast('oauth:login', service.token);
+ } else {
+ $rootScope.$broadcast('oauth:refresh', service.token);
+ }
+ return result.data;
+ }, function(error) {
+ if (!!service.typedLogin && !!service.typedPassword) {
+ return reconnect();
+ } else {
+ if (error.status === 401 || error.status === 400) {
+ cancelExpiresAtEvent();
+ Storage.delete('token');
+ $rootScope.$broadcast('oauth:expired');
+ service.runExpired();
+ }
+ }
+ });
+ } else {
+ var deferred = $q.defer();
+ deferred.reject();
+ return deferred.promise;
+ }
+ };
- /**
- * Save the access token into the session
- */
- var setTokenInSession = function(){
- Storage.set('token', service.token);
- };
+ var reconnect = function() {
+ return $http({
+ method: "POST",
+ url: refreshTokenUri,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ transformRequest: function(obj) {
+ var str = [];
+ for (var p in obj)
+ str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
+ return str.join("&");
+ },
+ data: {
+ grant_type: "password",
+ username: service.typedLogin,
+ password: service.typedPassword,
+ scope: service.scope
+ }
+ }).then(function(result) {
+ angular.extend(service.token, result.data);
+ setTokenInSession();
+ $rootScope.$broadcast('oauth:refresh', service.token);
+ }, function(error) {
+ if (!!service.typedLogin && !!service.typedPassword) {
+ return reconnect();
+ } else {
+ if (error.status === 401 || error.status === 400) {
+ cancelExpiresAtEvent();
+ Storage.delete('token');
+ $rootScope.$broadcast('oauth:expired');
+ service.runExpired();
+ }
+ }
+ });
+ };
- /**
- * Set the access token expiration date (useful for refresh logics)
- */
- var setExpiresAt = function(){
- if (!service.token) {
- return;
- }
- if(typeof(service.token.expires_in) !== 'undefined' && service.token.expires_in !== null) {
- var expires_at = new Date();
- expires_at.setSeconds(expires_at.getSeconds() + parseInt(service.token.expires_in)-60); // 60 seconds less to secure browser and response latency
- service.token.expires_at = expires_at;
- }
- else {
- service.token.expires_at = null;
- }
- };
+ /**
+ * Set the access token.
+ *
+ * @param params
+ * @returns {*|{}}
+ */
+ var setToken = function(params) {
+ service.token = service.token || {}; // init the token
+ angular.extend(service.token, params); // set the access token params
+ setTokenInSession(); // save the token into the session
+ setExpiresAt();
+ setExpiresAtEvent(); // event to fire when the token expires
+
+ return service.token;
+ };
+ /**
+ * Parse the fragment URI and return an object
+ * @param hash
+ * @returns {{}}
+ */
+ var getTokenFromString = function(hash) {
+ var params = {},
+ regex = /([^&=]+)=([^&]*)/g,
+ m;
- /**
- * Set the timeout at which the expired event is fired
- */
- var setExpiresAtEvent = function(){
- // Don't bother if there's no expires token
- if (typeof(service.token.expires_at) === 'undefined' || service.token.expires_at === null) {
- return;
- }
- cancelExpiresAtEvent();
- var time = (new Date(service.token.expires_at))-(new Date());
- if(time && time > 0 && time <= 2147483647){
- expiresAtEvent = $interval(function(){
- $rootScope.$broadcast('oauth:expired', service.token);
- }, time, 1);
- }
- };
+ while ((m = regex.exec(hash)) !== null) {
+ params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
+ }
- var cancelExpiresAtEvent = function() {
- if(expiresAtEvent) {
- $timeout.cancel(expiresAtEvent);
- expiresAtEvent = undefined;
- }
- };
+ // OpenID Connect
+ if (params.id_token && !params.error) {
+ IdToken.validateTokensAndPopulateClaims(params);
+ return params;
+ }
- /**
- * Remove the oAuth2 pieces from the hash fragment
- */
- var removeFragment = function(){
- var curHash = $location.hash();
- angular.forEach(hashFragmentKeys,function(hashKey){
- var re = new RegExp('&'+hashKey+'(=[^&]*)?|^'+hashKey+'(=[^&]*)?&?');
- curHash = curHash.replace(re,'');
- });
-
- $location.hash(curHash);
- };
+ // Oauth2
+ if (params.access_token || params.error) {
+ return params;
+ }
+ };
+
+ /**
+ * Save the access token into the session
+ */
+ var setTokenInSession = function() {
+ Storage.set('token', service.token);
+ };
+
+ /**
+ * Set the access token expiration date (useful for refresh logics)
+ */
+ var setExpiresAt = function() {
+ if (!service.token) {
+ return;
+ }
+ if (typeof(service.token.expires_in) !== 'undefined' && service.token.expires_in !== null) {
+ var expires_at = new Date();
+ expires_at.setSeconds(expires_at.getSeconds() + parseInt(service.token.expires_in) - 60); // 60 seconds less to secure browser and response latency
+ service.token.expires_at = expires_at;
+ } else {
+ service.token.expires_at = null;
+ }
+ };
- return service;
+
+ /**
+ * Set the interval at which the expired event is fired
+ */
+ var setExpiresAtEvent = function() {
+ // Don't bother if there's no expires token
+ if (typeof(service.token.expires_at) === 'undefined' || service.token.expires_at === null) {
+ return;
+ }
+ cancelExpiresAtEvent();
+ var time = (new Date(service.token.expires_at)) - (new Date());
+ if (time && time > 0 && time <= 2147483647) {
+ if (service.token.refresh_token) {
+ expiresAtEvent = $interval(function() {
+ refreshToken();
+ }, time);
+ } else {
+ expiresAtEvent = $timeout(function() {
+ $rootScope.$broadcast('oauth:expired');
+ service.runExpired();
+ }, time, 1);
+ }
+ }
+ };
+
+ var cancelExpiresAtEvent = function() {
+ if (expiresAtEvent) {
+ if (service.token.refresh_token) {
+ $interval.cancel(expiresAtEvent);
+ } else {
+ $timeout.cancel(expiresAtEvent);
+ }
+ expiresAtEvent = undefined;
+ }
+ };
+
+ /**
+ * Remove the oAuth2 pieces from the hash fragment
+ */
+ var removeFragment = function() {
+ var curHash = $location.hash();
+ angular.forEach(hashFragmentKeys, function(hashKey) {
+ var re = new RegExp('&' + hashKey + '(=[^&]*)?|^' + hashKey + '(=[^&]*)?&?');
+ curHash = curHash.replace(re, '');
+ });
+
+ $location.hash(curHash);
+ };
+
+ return service;
}]);
@@ -636,7 +806,7 @@ endpointClient.factory('Endpoint', ['$rootScope', 'AccessToken', '$q', '$http',
*/
service.checkValidity = function() {
var params = service.config;
- if( params.sessionPath ) {
+ if( params.sessionPath && !params.disableCheckSession ) {
var token = AccessToken.get();
if( !token ) {
return $q.reject("No token configured");
@@ -651,6 +821,8 @@ endpointClient.factory('Endpoint', ['$rootScope', 'AccessToken', '$q', '$http',
return $q.reject("Server replied: token is invalid.");
}
});
+ } else if (params.disableCheckSession) {
+ return true;
} else {
return $q.reject("You must give a :session-path param in order to validate the token.")
}
@@ -842,25 +1014,27 @@ directives.directive('oauth', [
restrict: 'AE',
replace: true,
scope: {
- site: '@', // (required) set the oauth server host (e.g. http://oauth.example.com)
- clientId: '@', // (required) client id
- redirectUri: '@', // (required) client redirect uri
- responseType: '@', // (optional) response type, defaults to token (use 'token' for implicit flow and 'code' for authorization code flow
- scope: '@', // (optional) scope
- profileUri: '@', // (optional) user profile uri (e.g http://example.com/me)
- template: '@', // (optional) template to render (e.g bower_components/oauth-ng/dist/views/templates/default.html)
- text: '@', // (optional) login text
- authorizePath: '@', // (optional) authorization url
- state: '@', // (optional) An arbitrary unique string created by your app to guard against Cross-site Request Forgery
- storage: '@', // (optional) Store token in 'sessionStorage' or 'localStorage', defaults to 'sessionStorage'
- nonce: '@', // (optional) Send nonce on auth request
- // OpenID Connect extras, more details in id-token.js:
- issuer: '@', // (optional for OpenID Connect) issuer of the id_token, should match the 'iss' claim in id_token payload
- subject: '@', // (optional for OpenID Connect) subject of the id_token, should match the 'sub' claim in id_token payload
- pubKey: '@', // (optional for OpenID Connect) the public key(RSA public key or X509 certificate in PEM format) to verify the signature
- wellKnown: '@', // (optional for OpenID Connect) whether to load public key according to .well-known/openid-configuration endpoint
- logoutPath: '@', // (optional) A url to go to at logout
- sessionPath: '@' // (optional) A url to use to check the validity of the current token.
+ site: '@', // (required) set the oauth server host (e.g. http://oauth.example.com)
+ clientId: '@', // (required) client id
+ redirectUri: '@', // (required) client redirect uri
+ responseType: '@', // (optional) response type, defaults to token (use 'token' for implicit flow, 'code' for authorization code flow and 'password' for resource owner password
+ scope: '@', // (optional) scope
+ profileUri: '@', // (optional) user profile uri (e.g http://example.com/me)
+ template: '@', // (optional) template to render (e.g bower_components/oauth-ng/dist/views/templates/default.html)
+ text: '@', // (optional) login text
+ authorizePath: '@', // (optional) authorization url
+ tokenPath: '@', // (optional) token url
+ state: '@', // (optional) An arbitrary unique string created by your app to guard against Cross-site Request Forgery
+ storage: '@', // (optional) Store token in 'sessionStorage' or 'localStorage', defaults to 'sessionStorage'
+ nonce: '@', // (optional) Send nonce on auth request
+ // OpenID Connect extras, more details in id-token.js:
+ issuer: '@', // (optional for OpenID Connect) issuer of the id_token, should match the 'iss' claim in id_token payload
+ subject: '@', // (optional for OpenID Connect) subject of the id_token, should match the 'sub' claim in id_token payload
+ pubKey: '@', // (optional for OpenID Connect) the public key(RSA public key or X509 certificate in PEM format) to verify the signature
+ wellKnown: '@', // (optional for OpenID Connect) whether to load public key according to .well-known/openid-configuration endpoint
+ logoutPath: '@', // (optional) A url to go to at logout
+ sessionPath: '@', // (optional) A url to use to check the validity of the current token.
+ disableCheckSession:'@' // (optional) can current token be checked ?
}
};
@@ -879,22 +1053,29 @@ directives.directive('oauth', [
OidcConfig.load(scope) // loads OIDC configuration from .well-known/openid-configuration if necessary
.then(function() {
IdToken.set(scope);
- AccessToken.set(scope); // sets the access token object (if existing, from fragment or session)
- initProfile(scope); // gets the profile resource (if existing the access token)
- initView(); // sets the view (logged in or out)
- checkValidity(); // ensure the validity of the current token
+ AccessToken.set(scope).then(function () { // sets the access token object (if existing, from fragment or session)
+ })
+ ["finally"](function () {
+ initProfile(scope); // gets the profile resource (if existing the access token)
+ initView(); // sets the view (logged in or out)
+ checkValidity(); // ensure the validity of the current token
+ });
});
};
var initAttributes = function() {
- scope.authorizePath = scope.authorizePath || '/oauth/authorize';
- scope.tokenPath = scope.tokenPath || '/oauth/token';
- scope.template = scope.template || 'bower_components/oauth-ng/dist/views/templates/default.html';
- scope.responseType = scope.responseType || 'token';
- scope.text = scope.text || 'Sign In';
- scope.state = scope.state || undefined;
- scope.scope = scope.scope || undefined;
- scope.storage = scope.storage || 'sessionStorage';
+ scope.authorizePath = scope.authorizePath || '/oauth/authorize';
+ scope.tokenPath = scope.tokenPath || '/oauth/token';
+ scope.template = scope.template || 'bower_components/oauth-ng/dist/views/templates/default.html';
+ scope.responseType = scope.responseType || 'token';
+ scope.text = scope.text || 'Sign In';
+ scope.state = scope.state || undefined;
+ scope.scope = scope.scope || undefined;
+ scope.storage = scope.storage || 'sessionStorage';
+ scope.disableCheckSession = scope.disableCheckSession || false;
+ scope.typedLogin = "";
+ scope.typedPassword = "";
+ scope.typedKeepConnection = false;
};
var compile = function() {
@@ -917,8 +1098,8 @@ directives.directive('oauth', [
var initView = function () {
var token = AccessToken.get();
- if (!token) {
- return scope.login();
+ if (!token && scope.responseType !== "password") {
+ return expired();
} // without access token it's logged out, so we attempt to log in
if (AccessToken.expired()) {
return expired();
@@ -936,12 +1117,43 @@ directives.directive('oauth', [
};
scope.logout = function () {
+ scope.typedLogin = "";
+ scope.typedPassword = "";
+ scope.typedKeepConnection = false;
Endpoint.logout();
$rootScope.$broadcast('oauth:loggedOut');
scope.show = 'logged-out';
+ AccessToken.destroy();
+ };
+
+ scope.checkPassword = function () {
+ $http({
+ method: "POST",
+ url: scope.site + scope.tokenPath,
+ headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+ transformRequest: function(obj) {
+ var str = [];
+ for(var p in obj)
+ str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
+ return str.join("&");
+ },
+ data: {grant_type: "password", username: scope.typedLogin, password: scope.typedPassword, scope: scope.scope}
+ }).then(function (result) {
+ if (scope.typedKeepConnection) {
+ AccessToken.setTokenFromPassword(scope, result.data, scope.typedLogin, scope.typedPassword, scope.scope);
+ } else {
+ AccessToken.setTokenFromPassword(scope, result.data);
+ scope.typedLogin = "";
+ scope.typedPassword = "";
+ scope.typedKeepConnection = false;
+ }
+ scope.show = "logged-in";
+ }, function () {
+ $rootScope.$broadcast('oauth:denied');
+ });
};
- scope.$on('oauth:expired',expired);
+ scope.$on('oauth:expired', expired);
// user is authorized
var authorized = function() {
@@ -950,9 +1162,13 @@ directives.directive('oauth', [
};
var expired = function() {
- $rootScope.$broadcast('oauth:expired');
+ scope.show = 'logged-out';
scope.logout();
};
+
+ scope.runExpired = function() {
+ expired();
+ };
// set the oauth directive to the denied status
var denied = function() {
@@ -986,6 +1202,7 @@ directives.directive('oauth', [
scope.$on('$stateChangeSuccess', function () {
$timeout(refreshDirective);
});
+
};
return definition;
diff --git a/dist/views/templates/button.html b/dist/views/templates/button.html
index ce7861a..f30d62d 100644
--- a/dist/views/templates/button.html
+++ b/dist/views/templates/button.html
@@ -1,5 +1,25 @@
- Login Button
- Logout {{profile.email}}
- Access denied.
+ Login Button
+ Logout {{profile.email}}
+ Access denied.
+
+
+
+
diff --git a/dist/views/templates/default.html b/dist/views/templates/default.html
index dbd71f2..96dbe03 100644
--- a/dist/views/templates/default.html
+++ b/dist/views/templates/default.html
@@ -1,5 +1,25 @@
- {{text}}
- Logout {{profile.email}}
- Access denied. Try again.
+ {{text}}
+ Logout {{profile.email}}
+ Access denied. Try again.
+
+
+
+