diff --git a/.gitignore b/.gitignore
index a98ab44..101d74e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,7 +29,6 @@ docs
app/bower_components
.editorconfig
.gitattributes
-.jshintrc
.tmp
# Dev
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..c82708b
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,23 @@
+{
+ "node": true,
+ "browser": true,
+ "esnext": true,
+ "bitwise": true,
+ "camelcase": false,
+ "curly": true,
+ "eqeqeq": true,
+ "immed": true,
+ "indent": 2,
+ "latedef": true,
+ "newcap": true,
+ "noarg": true,
+ "quotmark": "single",
+ "undef": true,
+ "unused": true,
+ "strict": true,
+ "trailing": true,
+ "smarttabs": true,
+ "globals": {
+ "angular": false
+ }
+}
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 83f4e22..244b7e8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,3 @@
language: node_js
node_js:
- - '0.8'
- '0.10'
-before_script:
- - 'npm install -g bower grunt-cli'
- - 'bower install'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f23b23a..5cf1d51 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,18 +1,111 @@
# Changelog
+## 0.4.10 (Apr 25, 2016)
+
+* Support for loading keys from `.well-known/openid-configuration`
+
+## 0.4.9 (Mar 13, 2016)
+
+* Add DI explicit annotation to Endpoint factory
+* Add missing dependency to Access Token service
+
+## 0.4.8 (Feb 12, 2016)
+
+* Fixed issues related to state change
+
+## 0.4.7 (Feb 11, 2016)
+
+* Add logout capability
+* Add session validity checking feature
+
+## 0.4.6 (Jan 07, 2016)
+
+* Fix JWK format support of OIDC public key
+
+## 0.4.5 (Dec 16, 2015)
+
+* Add OpenID support
+
+## 0.4.4 (Nov 19, 2015)
+
+* Webpack Fix
+
+## 0.4.3 (Nov 19, 2015)
+
+* Added nonce parameter to directive
+
+## 0.4.2 (Jun 19, 2015)
+
+* Make the code more JSHint friendly
+* Fix expiry definition in the access token service
+
+## 0.4.1 (Jun 11, 2015)
+
+* Add OpenID support
+
+## 0.4.0 (May 26, 2015)
+
+* Fix jshint config file
+* Token `expires_in` property is now optional
+
+## 0.3.10 (April 20, 2015)
+
+* Add Storage service
+
+## 0.3.9 (April 14, 2015)
+
+* Add inline annotations for dependency injection
+
+## 0.3.8 (February 06, 2015)
+
+* Upgrade to AngularJS v1.3.12.
+
+## 0.3.6 (Dicember 03, 2014)
+
+* Broadcast event oauth:tokenDestroy after a logout.
+
+## 0.3.5 (November 29, 2014)
+
+* Remove access token and change directive text to 'logout' when token is expired.
+
+## 0.3.3 (November 25, 2014)
+
+* Add Fixed Code method option
+
+## 0.3.2 (November 16, 2014)
+
+* Add Authorization Code method option
+
+## 0.3.1 (November 3, 2014)
+
+* Replace $timeout with $interval #50
+* Add broadcast “oath:profile” once profile is retrieved. #51
+* Add travis
+
+## 0.3.0 (October 30, 2014)
+
+* Fix bug on access token definition from hash
+* Correctly running tests with E2E protractor
+
+## 0.2.8 (August 27, 2014)
+
+* Fix `expries_at` not being set in some situations
+* Only use session storage when oAuth hash not in URL
+* Only remove oAuth2 tokens from hash
+
## 0.2.7 (August 26, 2014)
-* Fixed `expires_at` not being set
-* Fixed `expired()` calculation
+* Fix `expires_at` not being set
+* Fix `expired()` calculation
## 0.2.6 (August 14, 2014)
-* Removed encoding for OAuth 2.0 scope.
+* Remove encoding for OAuth 2.0 scope.
## 0.2.4 (August 13, 2014)
-* Removed settings for HTML5 mode
-* Added logic to fire the oauth:expired event when the token expires. Before it was raised
+* Remove settings for HTML5 mode
+* Add logic to fire the oauth:expired event when the token expires. Before it was raised
only when the request was returning a 401.
## 0.2.2 (July 11, 2014)
@@ -28,6 +121,6 @@ per https://github.com/andreareginato/oauth-ng/issues/16
## 0.2.0 (June 1, 2014)
-* Updated name from ng-oauth to oauth-ng
+* Update name from ng-oauth to oauth-ng
* New documentation site andreareginato.github.io/oauth-ng
* Major refactoring
diff --git a/Gruntfile.js b/Gruntfile.js
index bd21cae..ffd8782 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -23,7 +23,7 @@ module.exports = function (grunt) {
};
try {
- var component = require('./bower.json')
+ var component = require('./bower.json');
yeomanConfig.name = component.name || 'no-name';
yeomanConfig.version = component.version || '0.0.0.undefined';
} catch (e) {}
@@ -114,6 +114,7 @@ module.exports = function (grunt) {
],
test: {
options: {
+ jasmine: true,
jshintrc: 'test/.jshintrc'
},
src: ['test/spec/{,*/}*.js']
@@ -229,9 +230,14 @@ module.exports = function (grunt) {
// Test settings
karma: {
- unit: {
+ options: {
configFile: 'karma.conf.js',
+ },
+ unit: {
singleRun: false
+ },
+ once: {
+ singleRun: true
}
},
@@ -278,7 +284,15 @@ module.exports = function (grunt) {
'concurrent:test',
'autoprefixer',
'connect:test',
- 'karma'
+ 'karma:once'
+ ]);
+
+ grunt.registerTask('test:unit', [
+ 'clean:server',
+ 'concurrent:test',
+ 'autoprefixer',
+ 'connect:test',
+ 'karma:unit'
]);
grunt.registerTask('build', [
diff --git a/README.md b/README.md
index ffabaae..bd45c18 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,11 @@
-# AngularJS directive for OAuth 2.0
+# AngularJS directive for OAuth 2.0 [](https://travis-ci.org/andreareginato/oauth-ng)
+
AngularJS directive for the [OAuth 2.0 Implicit Flow](http://tools.ietf.org/html/rfc6749#section-1.3.2).
## Documentation
-[](https://andreareginato.github.com/oauth-ng)
+[](https://angularjs-oauth.github.io/oauth-ng)
## Contributing
@@ -17,6 +18,23 @@ Please also update `gh-pages` branch with documentation when applicable.
* Fork and clone the repository
* Run `npm install && bower install`
+### OAuth 2.0 supported grant types
+
+We support both [OAuth 2.0 Authorization code Flow](http://tools.ietf.org/html/rfc6749#section-1.3.1)
+and the [OAuth 2.0 Implicit Flow](http://tools.ietf.org/html/rfc6749#section-1.3.2).
+
+#### Authorization code flow
+
+See: http://tools.ietf.org/html/rfc6749#section-4.1
+
+To use the Authorization code flow set response-type="code" in the oauth directive.
+
+#### Implicit flow
+
+See: http://tools.ietf.org/html/rfc6749#section-4.2
+
+To use the Implicit flow set response-type="token" in the oauth directive.
+
### Unit tests (karma)
`npm install && bower install`
@@ -36,14 +54,14 @@ Follow [github](https://github.com/styleguide/) guidelines.
### Feedback
-Use the [issue tracker](http://github.com/andreareginato/oauth-ng/issues) for bugs.
+Use the [issue tracker](http://github.com/angularjs-oauth/oauth-ng/issues) for bugs.
[Mail](mailto:andrea.reginato@gmail.com) or [Tweet](http://twitter.com/andreareginato) us for any idea
that can improve the project.
### Links
-* [GIT Repository](http://github.com/andreareginato/oauth-ng)
-* [Website](https://andreareginato.github.com/oauth-ng)
+* [GIT Repository](http://github.com/angularjs-oauth/oauth-ng)
+* [Website](https://angularjs-oauth.github.io/oauth-ng)
## Authors
@@ -51,18 +69,22 @@ that can improve the project.
Project created and released as open-source thanks to [Lelylan](http://lelylan.com).
* [Andrea Reginato](http://twitter.com/andreareginato)
+* [Massimiliano Sartoretto](http://twitter.com/___Sarto)
## Contributors
-Special thanks to all [contributors](https://github.com/andreareginato/oauth-ng/contributors)
+Special thanks to all [contributors](https://github.com/angularjs-oauth/oauth-ng/contributors)
for submitting patches.
## Changelog
-See [CHANGELOG](https://github.com/andreareginato/oauth-ng/blob/master/CHANGELOG.md)
+See [CHANGELOG](https://github.com/angularjs-oauth/oauth-ng/blob/master/CHANGELOG.md)
+
+## TODO
+:white_medium_square: [OAuth 2.0 Authorization code Flow](http://tools.ietf.org/html/rfc6749#section-1.3.1)
## Copyright
Copyright (c) 2014 [Lelylan](http://lelylan.com).
-See [LICENSE](https://github.com/andreareginato/oauth-ng/blob/master/LICENSE.md) for details.
+See [LICENSE](https://github.com/angularjs-oauth/oauth-ng/blob/master/LICENSE.md) for details.
diff --git a/app/index.html b/app/index.html
index 037b7cb..6b1ce97 100644
--- a/app/index.html
+++ b/app/index.html
@@ -17,6 +17,7 @@
+
@@ -32,20 +33,26 @@
client-id="CLIENT_ID_HERE"
redirect-uri="REDIRECT_URI_HERE"
profile-uri="PROFILE_URI_HERE"
- scope="SCOPE_HERE">
+ scope="SCOPE_HERE"
+ storage="STORAGE_TYPE_HERE">
+
+
+
+
+
diff --git a/app/scripts/app.js b/app/scripts/app.js
index 3755008..2d70a1e 100644
--- a/app/scripts/app.js
+++ b/app/scripts/app.js
@@ -1,15 +1,18 @@
'use strict';
// App libraries
-var app = angular.module('oauth', [
+angular.module('oauth', [
'oauth.directive', // login directive
+ 'oauth.idToken', // id token service (only for OpenID Connect)
+ 'oauth.oidcConfig', // for loading OIDC configuration from .well-known/openid-configuration endpoint
'oauth.accessToken', // access token service
'oauth.endpoint', // oauth endpoint service
'oauth.profile', // profile model
- 'oauth.interceptor' // bearer token interceptor
+ 'oauth.storage', // storage
+ 'oauth.interceptor', // bearer token interceptor
+ 'oauth.configuration' // token appender
])
-
-angular.module('oauth').config(['$locationProvider','$httpProvider',
+ .config(['$locationProvider','$httpProvider',
function($locationProvider, $httpProvider) {
$httpProvider.interceptors.push('ExpiredInterceptor');
}]);
diff --git a/app/scripts/directives/oauth.js b/app/scripts/directives/oauth.js
index 08cf7c0..ce2ff26 100644
--- a/app/scripts/directives/oauth.js
+++ b/app/scripts/directives/oauth.js
@@ -2,111 +2,171 @@
var directives = angular.module('oauth.directive', []);
-directives.directive('oauth', function(AccessToken, Endpoint, Profile, $location, $rootScope, $compile, $http, $templateCache) {
-
- var definition = {
- 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
- 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
- }
- };
-
- definition.link = function postLink(scope, element, attrs) {
- scope.show = 'none';
-
- scope.$watch('clientId', function(value) { init() });
-
- var init = function() {
- initAttributes(); // sets defaults
- compile(); // compiles the desired layout
- Endpoint.set(scope); // sets the oauth authorization url
- 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)
- };
-
- var initAttributes = function() {
- scope.authorizePath = scope.authorizePath || '/oauth/authorize';
- scope.tokenPath = scope.tokenPath || '/oauth/token';
- scope.template = scope.template || 'views/templates/default.html';
- scope.text = scope.text || 'Sign In';
- scope.state = scope.state || undefined;
- scope.scope = scope.scope || undefined;
- };
-
- var compile = function() {
- $http.get(scope.template, { cache: $templateCache }).success(function(html) {
- element.html(html);
- $compile(element.contents())(scope);
- });
- };
-
- var initProfile = function(scope) {
- var token = AccessToken.get();
-
- if (token && token.access_token && scope.profileUri) {
- Profile.find(scope.profileUri).success(function(response) {
- scope.profile = response
- })
+directives.directive('oauth', [
+ 'IdToken',
+ 'AccessToken',
+ 'Endpoint',
+ 'Profile',
+ 'Storage',
+ 'OidcConfig',
+ '$location',
+ '$rootScope',
+ '$compile',
+ '$http',
+ '$templateCache',
+ '$timeout',
+ function(IdToken, AccessToken, Endpoint, Profile, Storage, OidcConfig, $location, $rootScope, $compile, $http, $templateCache, $timeout) {
+
+ var definition = {
+ 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.
}
};
- var initView = function() {
- var token = AccessToken.get();
-
- if (!token) { return loggedOut() } // without access token it's logged out
- if (token.access_token) { return authorized() } // if there is the access token we are done
- if (token.error) { return denied() } // if the request has been denied we fire the denied event
- };
+ definition.link = function postLink(scope, element) {
+ scope.show = 'none';
- scope.login = function() {
- Endpoint.redirect();
- };
-
- scope.logout = function() {
- AccessToken.destroy(scope);
- loggedOut();
- };
+ scope.$watch('clientId', function() {
+ init();
+ });
- // user is authorized
- var authorized = function() {
- $rootScope.$broadcast('oauth:authorized', AccessToken.get());
- scope.show = 'logged-in';
- };
+ var init = function() {
+ initAttributes(); // sets defaults
+ Storage.use(scope.storage);// set storage
+ compile(); // compiles the desired layout
+ Endpoint.set(scope); // sets the oauth authorization url
+ 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
+ });
+ };
+
+ 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';
+ };
+
+ var compile = function() {
+ $http.get(scope.template, { cache: $templateCache }).then(function(html) {
+ element.html(html.data);
+ $compile(element.contents())(scope);
+ });
+ };
+
+ var initProfile = function(scope) {
+ var token = AccessToken.get();
+
+ if (token && token.access_token && scope.profileUri) {
+ Profile.find(scope.profileUri).then(function(response) {
+ scope.profile = response.data;
+ });
+ }
+ };
+
+ var initView = function () {
+ var token = AccessToken.get();
+
+ if (!token) {
+ return scope.login();
+ } // without access token it's logged out, so we attempt to log in
+ if (AccessToken.expired()) {
+ return expired();
+ } // with a token, but it's expired
+ if (token.access_token) {
+ return authorized();
+ } // if there is the access token we are done
+ if (token.error) {
+ return denied();
+ } // if the request has been denied we fire the denied event
+ };
+
+ scope.login = function () {
+ Endpoint.redirect();
+ };
+
+ scope.logout = function () {
+ Endpoint.logout();
+ $rootScope.$broadcast('oauth:loggedOut');
+ scope.show = 'logged-out';
+ };
+
+ scope.$on('oauth:expired',expired);
+
+ // user is authorized
+ var authorized = function() {
+ $rootScope.$broadcast('oauth:authorized', AccessToken.get());
+ scope.show = 'logged-in';
+ };
+
+ var expired = function() {
+ $rootScope.$broadcast('oauth:expired');
+ scope.logout();
+ };
+
+ // set the oauth directive to the denied status
+ var denied = function() {
+ scope.show = 'denied';
+ $rootScope.$broadcast('oauth:denied');
+ };
+
+ var checkValidity = function() {
+ Endpoint.checkValidity().then(function() {
+ $rootScope.$broadcast('oauth:valid');
+ }).catch(function(message){
+ $rootScope.$broadcast('oauth:invalid', message);
+ });
+ };
+
+ var refreshDirective = function () {
+ scope.$apply();
+ };
+
+ // Updates the template at runtime
+ scope.$on('oauth:template:update', function(event, template) {
+ scope.template = template;
+ compile(scope);
+ });
- // set the oauth directive to the logged-out status
- var loggedOut = function() {
- $rootScope.$broadcast('oauth:logout');
- scope.show = 'logged-out';
- };
+ // Hack to update the directive content on logout
+ scope.$on('$routeChangeSuccess', function () {
+ $timeout(refreshDirective);
+ });
- // set the oauth directive to the denied status
- var denied = function() {
- scope.show = 'denied';
- $rootScope.$broadcast('oauth:denied');
+ scope.$on('$stateChangeSuccess', function () {
+ $timeout(refreshDirective);
+ });
};
- // Updates the template at runtime
- scope.$on('oauth:template:update', function(event, template) {
- scope.template = template;
- compile(scope);
- });
-
- // Hack to update the directive content on logout
- // TODO think to a cleaner solution
- scope.$on('$routeChangeSuccess', function () {
- init();
- });
- };
-
- return definition
-});
+ return definition;
+ }
+]);
diff --git a/app/scripts/interceptors/oauth-interceptor.js b/app/scripts/interceptors/oauth-interceptor.js
index ba27246..be3d8a9 100644
--- a/app/scripts/interceptors/oauth-interceptor.js
+++ b/app/scripts/interceptors/oauth-interceptor.js
@@ -2,22 +2,23 @@
var interceptorService = angular.module('oauth.interceptor', []);
-interceptorService.factory('ExpiredInterceptor', function ($rootScope, $q, $sessionStorage) {
+interceptorService.factory('ExpiredInterceptor', ['Storage', '$rootScope', function (Storage, $rootScope) {
var service = {};
service.request = function(config) {
- var token = $sessionStorage.token;
+ var token = Storage.get('token');
- if (token && expired(token))
+ if (token && expired(token)) {
$rootScope.$broadcast('oauth:expired', token);
+ }
return config;
};
var expired = function(token) {
- return (token && token.expires_at && new Date(token.expires_at) < new Date())
+ return (token && token.expires_at && new Date(token.expires_at) < new Date());
};
return service;
-});
+}]);
diff --git a/app/scripts/services/access-token.js b/app/scripts/services/access-token.js
index 9bf875a..7e34d9b 100644
--- a/app/scripts/services/access-token.js
+++ b/app/scripts/services/access-token.js
@@ -1,164 +1,204 @@
'use strict';
-var accessTokenService = angular.module('oauth.accessToken', ['ngStorage']);
-
-accessTokenService.factory('AccessToken', function($rootScope, $location, $sessionStorage, $timeout) {
-
- var service = {};
- var token = null;
-
-
- /*
+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;
+ };
- service.get = function() {
- return 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());
- service.set = function() {
- service.setTokenFromString($location.hash());
- service.setTokenFromSession();
- return token
- }
+ //If hash is present in URL always use it, cuz its coming from oAuth2 provider redirect
+ if(null === service.token){
+ setTokenFromSession();
+ }
+ return this.token;
+ };
- /*
- * Delete the access token and remove the session.
+ /**
+ * Delete the access token and remove the session.
+ * @returns {null}
*/
+ service.destroy = function(){
+ Storage.delete('token');
+ this.token = null;
+ return this.token;
+ };
- service.destroy = function() {
- delete $sessionStorage.token;
- return token = null;
- }
-
-
- /*
+ /**
* 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.expired = function() {
- return (token && token.expires_at && 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 *
* * * * * * * * * */
-
- /*
- * Get the access token from a string and save it
+ /**
+ * Set the access token from the sessionStorage.
*/
-
- service.setTokenFromString = function(hash) {
- var token = getTokenFromString(hash);
-
- if (token) {
- removeFragment();
- service.setToken(token);
- setExpiresAt(token);
- $rootScope.$broadcast('oauth:login', token);
+ 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;
- var getTokenFromString = function(hash) {
- var splitted = hash.split('&');
- var params = {};
-
- for (var i = 0; i < splitted.length; i++) {
- var param = splitted[i].split('=');
- var key = param[0];
- var value = param[1];
- params[key] = value
+ while ((m = regex.exec(hash)) !== null) {
+ params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
}
- if (params.access_token || params.error)
+ // OpenID Connect
+ if (params.id_token && !params.error) {
+ IdToken.validateTokensAndPopulateClaims(params);
return params;
- }
-
-
- /*
- * Set the access token from the sessionStorage.
- */
-
- service.setTokenFromSession = function() {
- if ($sessionStorage.token) {
- var params = $sessionStorage.token;
- params.expires_at = new Date(params.expires_at);
- service.setToken(params);
}
- }
+ // Oauth2
+ if(params.access_token || params.error){
+ return params;
+ }
+ };
- /*
+ /**
* Save the access token into the session
*/
-
- var setTokenInSession = function() {
- $sessionStorage.token = token;
- }
-
-
- /*
- * Set the access token.
- */
-
- service.setToken = function(params) {
- token = token || {}; // init the token
- angular.extend(token, params); // set the access token params
- setTokenInSession(); // save the token into the session
- setExpiresAtEvent(); // event to fire when the token expires
-
- return token;
+ var setTokenInSession = function(){
+ Storage.set('token', service.token);
};
-
- /*
+ /**
* Set the access token expiration date (useful for refresh logics)
*/
-
- var setExpiresAt = function(token) {
- if (token) {
+ 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(token.expires_in) - 60); // 60 seconds less to secure browser and response latency
- token.expires_at = expires_at;
+ 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 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);
+ }
+ };
- var setExpiresAtEvent = function() {
- var time = (new Date(token.expires_at)) - (new Date())
- if (time) { $timeout(function() { $rootScope.$broadcast('oauth:expired', token) }, time) }
- }
-
+ var cancelExpiresAtEvent = function() {
+ if(expiresAtEvent) {
+ $timeout.cancel(expiresAtEvent);
+ expiresAtEvent = undefined;
+ }
+ };
- /*
- * Remove the fragment URI
- * TODO we need to remove only the access token
+ /**
+ * Remove the oAuth2 pieces from the hash fragment
*/
-
- var removeFragment = function(scope) {
- $location.hash('');
- }
-
+ 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 5353e40..6db8df6 100644
--- a/app/scripts/services/endpoint.js
+++ b/app/scripts/services/endpoint.js
@@ -2,49 +2,105 @@
var endpointClient = angular.module('oauth.endpoint', []);
-endpointClient.factory('Endpoint', function(AccessToken, $location) {
+endpointClient.factory('Endpoint', ['$rootScope', 'AccessToken', '$q', '$http', function($rootScope, AccessToken, $q, $http) {
var service = {};
- var url;
+ var buildOauthUrl = function (path, params) {
+ var oAuthScope = (params.scope) ? encodeURIComponent(params.scope) : '',
+ state = (params.state) ? encodeURIComponent(params.state) : '',
+ authPathHasQuery = (params.authorizePath.indexOf('?') == -1) ? false : true,
+ appendChar = (authPathHasQuery) ? '&' : '?', //if authorizePath has ? already append OAuth2 params
+ nonceParam = (params.nonce) ? '&nonce=' + params.nonce : '',
+ responseType = encodeURIComponent(params.responseType);
+
+ return params.site +
+ path +
+ appendChar + 'response_type=' + responseType + '&' +
+ 'client_id=' + encodeURIComponent(params.clientId) + '&' +
+ 'redirect_uri=' + encodeURIComponent(params.redirectUri) + '&' +
+ 'scope=' + oAuthScope + '&' +
+ 'state=' + state + nonceParam;
+ };
+
+ var extendValidity = function (tokenInfo) {
+ AccessToken.updateExpiry(tokenInfo.expires);
+ };
/*
* Defines the authorization URL
*/
- service.set = function(scope) {
- var oAuthScope = (scope.scope) ? scope.scope : '',
- state = (scope.state) ? encodeURIComponent(scope.state) : '',
- authPathHasQuery = (scope.authorizePath.indexOf('?') == -1) ? false : true,
- appendChar = (authPathHasQuery) ? '&' : '?'; //if authorizePath has ? already append OAuth2 params
+ service.set = function(configuration) {
+ this.config = configuration;
+ return this.get();
+ };
- url = scope.site +
- scope.authorizePath +
- appendChar + 'response_type=token&' +
- 'client_id=' + encodeURIComponent(scope.clientId) + '&' +
- 'redirect_uri=' + encodeURIComponent(scope.redirectUri) + '&' +
- 'scope=' + oAuthScope + '&' +
- 'state=' + state;
+ /*
+ * Returns the authorization URL
+ */
- return url;
+ service.get = function(overrides) {
+ var params = angular.extend( {}, service.config, overrides);
+ return buildOauthUrl(params.authorizePath, params);
};
/*
- * Returns the authorization URL
+ * Redirects the app to the authorization URL
*/
- service.get = function() {
- return url;
+ service.redirect = function(overrides) {
+ var targetLocation = this.get(overrides);
+ $rootScope.$broadcast('oauth:logging-in');
+ window.location.replace(targetLocation);
};
+ /*
+ * Alias for 'redirect'
+ */
+ service.login = function() {
+ service.redirect();
+ };
/*
- * Redirects the app to the authorization URL
+ * Check the validity of the token if a session path is available
+ */
+ service.checkValidity = function() {
+ var params = service.config;
+ if( params.sessionPath ) {
+ var token = AccessToken.get();
+ if( !token ) {
+ return $q.reject("No token configured");
+ }
+ var path = params.site + params.sessionPath + "?token=" + token.access_token;
+ return $http.get(path).then( function(httpResponse) {
+ var tokenInfo = httpResponse.data;
+ if(tokenInfo.valid) {
+ extendValidity(tokenInfo);
+ return true;
+ } else {
+ return $q.reject("Server replied: token is invalid.");
+ }
+ });
+ } else {
+ return $q.reject("You must give a :session-path param in order to validate the token.")
+ }
+ };
+
+ /*
+ * Destroys the session, sends the user to the logout url if set.
+ * First broadcasts 'logging-out' and then 'logout' when finished.
*/
- service.redirect = function() {
- window.location.replace(url);
+ service.logout = function() {
+ var params = service.config;
+ AccessToken.destroy();
+ $rootScope.$broadcast('oauth:logging-out');
+ if( params.logoutPath ) {
+ window.location.replace(buildOauthUrl(params.logoutPath, params));
+ }
+ $rootScope.$broadcast('oauth:logout');
};
return service;
-});
+}]);
diff --git a/app/scripts/services/id-token.js b/app/scripts/services/id-token.js
new file mode 100644
index 0000000..d6fada3
--- /dev/null
+++ b/app/scripts/services/id-token.js
@@ -0,0 +1,271 @@
+'use strict';
+
+var idTokenService = angular.module('oauth.idToken', []);
+
+idTokenService.factory('IdToken', ['Storage', function(Storage) {
+
+ var service = {
+ issuer: null,
+ subject: null,
+ //clientId, should match 'aud' claim
+ clientId: null,
+ /*
+ The public key to verify the signature, supports:
+ 1.RSA public key in PEM string: e.g. "-----BEGIN PUBLIC KEY..."
+ 2.X509 certificate in PEM string: e.g. "-----BEGIN CERTIFICATE..."
+ 3.JWK (Json Web Key): e.g. {kty: "RSA", n: "0vx7...", e: "AQAB"}
+
+ If not set, the id_token header should carry the key or the url to retrieve the key
+ */
+ pubKey: null
+ };
+ /**
+ * OidcException
+ * @param {string } message - The exception error message
+ * @constructor
+ */
+ function OidcException(message) {
+ this.name = 'OidcException';
+ this.message = message;
+ }
+ OidcException.prototype = new Error();
+ OidcException.prototype.constructor = OidcException;
+
+ /**
+ * For initialization
+ * @param scope
+ */
+ service.set = function(scope) {
+ this.issuer = scope.issuer;
+ this.subject = scope.subject;
+ this.clientId = scope.clientId;
+ this.pubKey = scope.pubKey;
+ };
+
+ /**
+ * Validate id_token and access_token(if there's one)
+ * If validation passes, the id_token payload(claims) will be populated to 'params'
+ * Otherwise error will set to 'params' and tokens will be removed
+ *
+ * @param params
+ */
+ service.validateTokensAndPopulateClaims = function(params) {
+ var valid = false;
+ var message = '';
+ try {
+ valid = this.validateIdToken(params.id_token);
+ /*
+ if response_type is 'id_token token', then we will get both id_token and access_token,
+ access_token needs to be validated as well
+ */
+ if (valid && params.access_token) {
+ valid = this.validateAccessToken(params.id_token, params.access_token);
+ }
+ } catch (error) {
+ message = error.message;
+ }
+
+ if (valid) {
+ params.id_token_claims = getIdTokenPayload(params.id_token);
+ } else {
+ params.id_token = null;
+ params.access_token = null;
+ params.error = 'Failed to validate token:' + message;
+ }
+ };
+
+
+ /**
+ * Validates the id_token
+ * @param {String} idToken The id_token
+ * @returns {boolean} True if all the check passes, False otherwise
+ */
+ service.validateIdToken = function(idToken) {
+ return this.verifyIdTokenSig(idToken) && this.verifyIdTokenInfo(idToken);
+ };
+
+ /**
+ * Validate access_token based on the 'alg' and 'at_hash' value of the id_token header
+ * per spec: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitTokenValidation
+ *
+ * @param idToken The id_token
+ * @param accessToken The access_token
+ * @returns {boolean} true if validation passes
+ */
+ service.validateAccessToken = function(idToken, accessToken) {
+ var header = getJsonObject(getIdTokenParts(idToken)[0]);
+ if (header.at_hash) {
+ var shalevel = header.alg.substr(2);
+ if (shalevel !== '256' && shalevel !== '384' && shalevel !== '512') {
+ throw new OidcException('Unsupported hash algorithm, expecting sha256, sha384, or sha512');
+ }
+ var md = new KJUR.crypto.MessageDigest({'alg':'sha'+ shalevel, 'prov':'cryptojs'});
+ //hex representation of the hash
+ var hexStr = md.digestString(accessToken);
+ //take first 128bits and base64url encoding it
+ var expected = hextob64u(hexStr.substring(0, 32));
+
+ return expected === header.at_hash;
+ } else {
+ return true;
+ }
+ };
+
+ /**
+ * Verifies the ID Token signature using the specified public key
+ * The id_token header can optionally carry public key or the url to retrieve the public key
+ * Otherwise will use the public key configured using 'pubKey'
+ *
+ * Supports only RSA signatures ['RS256', 'RS384', 'RS512']
+ * @param {string}idToken The ID Token string
+ * @returns {boolean} Indicates whether the signature is valid or not
+ * @throws {OidcException}
+ */
+ service.verifyIdTokenSig = function (idToken) {
+
+ var idtParts = getIdTokenParts(idToken);
+ var header = getJsonObject(idtParts[0]);
+
+ if(!header.alg || header.alg.substr(0, 2) !== 'RS') {
+ throw new OidcException('Unsupported JWS signature algorithm ' + header.alg);
+ }
+
+ var matchedPubKey = null;
+
+ if (header.jwk) {
+ //Take the JWK if it comes with the id_token
+ matchedPubKey = header.jwk;
+ if (matchedPubKey.kid && header.kid && matchedPubKey.kid !== header.kid) {
+ throw new OidcException('Json Web Key ID not match');
+ }
+ /*
+ TODO: Support for "jku" (JWK Set URL), "x5u" (X.509 URL), "x5c" (X.509 Certificate Chain) parameter to get key
+ per http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-26#page-9
+ */
+ } else {
+ //Try to load the key from .well-known configuration
+ var oidcConfig = Storage.get('oidcConfig');
+ if (angular.isDefined(oidcConfig) && oidcConfig.jwks && oidcConfig.jwks.keys) {
+ oidcConfig.jwks.keys.forEach(function(key) {
+ if (key.kid === header.kid) {
+ matchedPubKey = key;
+ }
+ });
+ } else {
+ //Use configured public key
+ var jwk = getJsonObject(this.pubKey);
+ matchedPubKey = jwk ? jwk : this.pubKey; //JWK or PEM
+ }
+ }
+
+ if(!matchedPubKey) {
+ throw new OidcException('No public key found to verify signature');
+ }
+
+ return rsaVerifyJWS(idToken, matchedPubKey, header.alg);
+ };
+
+ /**
+ * Validates the information in the ID Token against configuration
+ * @param {string} idtoken The ID Token string
+ * @returns {boolean} Validity of the ID Token
+ * @throws {OidcException}
+ */
+ service.verifyIdTokenInfo = function(idtoken) {
+ var valid = false;
+
+ if (idtoken) {
+ var idtParts = getIdTokenParts(idtoken);
+ var payload = getJsonObject(idtParts[1]);
+ if (payload) {
+ var now = new Date() / 1000;
+ if (payload.iat > now + 60)
+ throw new OidcException('ID Token issued time is later than current time');
+
+ if (payload.exp < now )
+ throw new OidcException('ID Token expired');
+
+ if (now < payload.ntb)
+ throw new OidcException('ID Token is invalid before '+ payload.ntb);
+
+ if (payload.iss && this.issuer && payload.iss !== this.issuer)
+ throw new OidcException('Invalid issuer ' + payload.iss + ' != ' + this.issuer);
+
+ if (payload.sub && this.subject && payload.sub !== this.subject)
+ throw new OidcException('Invalid subject ' + payload.sub + ' != ' + this.subject);
+
+ if (payload.aud) {
+ if (payload.aud instanceof Array && !KJUR.jws.JWS.inArray(this.clientId, payload.aud)) {
+ throw new OidcException('Client not in intended audience:' + payload.aud);
+ }
+ if (typeof payload.aud === 'string' && payload.aud !== this.clientId) {
+ throw new OidcException('Invalid audience ' + payload.aud + ' != ' + this.clientId);
+ }
+ }
+
+ //TODO: nonce support ? probably need to redo current nonce support
+ //if(payload['nonce'] != sessionStorage['nonce'])
+ // throw new OidcException('invalid nonce');
+ valid = true;
+ } else
+ throw new OidcException('Unable to parse JWS payload');
+ }
+ return valid;
+ };
+
+ /**
+ * Verifies the JWS string using the JWK
+ * @param {string} jws The JWS string
+ * @param {object} pubKey The public key that will be used to verify the signature
+ * @param {string} alg The algorithm string. Expecting 'RS256', 'RS384', or 'RS512'
+ * @returns {boolean} Validity of the JWS signature
+ * @throws {OidcException}
+ */
+ var rsaVerifyJWS = function (jws, pubKey, alg) {
+ /*
+ convert various public key format to RSAKey object
+ see @KEYUTIL.getKey for a full list of supported input format
+ */
+ var rsaKey = KEYUTIL.getKey(pubKey);
+
+ return KJUR.jws.JWS.verify(jws, rsaKey, [alg]);
+ };
+
+ /**
+ * Splits the ID Token string into the individual JWS parts
+ * @param {string} id_token ID Token
+ * @returns {Array} An array of the JWS compact serialization components (header, payload, signature)
+ */
+ var getIdTokenParts = function (id_token) {
+ var jws = new KJUR.jws.JWS();
+ jws.parseJWS(id_token);
+ return [jws.parsedJWS.headS, jws.parsedJWS.payloadS, jws.parsedJWS.si];
+ };
+
+ /**
+ * Get the contents of the ID Token payload as an JSON object
+ * @param {string} id_token ID Token
+ * @returns {object} The ID Token payload JSON object
+ */
+ var getIdTokenPayload = function (id_token) {
+ var parts = getIdTokenParts(id_token);
+ if(parts)
+ return getJsonObject(parts[1]);
+ };
+
+ /**
+ * Get the JSON object from the JSON string
+ * @param {string} jsonS JSON string
+ * @returns {object|null} JSON object or null
+ */
+ var getJsonObject = function (jsonS) {
+ var jws = KJUR.jws.JWS;
+ if(jws.isSafeJSONString(jsonS)) {
+ return jws.readSafeJSONString(jsonS);
+ }
+ return null;
+ };
+
+ return service;
+
+}]);
diff --git a/app/scripts/services/oauth-configuration.js b/app/scripts/services/oauth-configuration.js
new file mode 100644
index 0000000..2cdc22a
--- /dev/null
+++ b/app/scripts/services/oauth-configuration.js
@@ -0,0 +1,38 @@
+'use strict';
+
+var oauthConfigurationService = angular.module('oauth.configuration', []);
+
+oauthConfigurationService.provider('OAuthConfiguration', function() {
+ var _config = {};
+
+ this.init = function(config, httpProvider) {
+ _config.protectedResources = config.protectedResources || [];
+ httpProvider.interceptors.push('AuthInterceptor');
+ };
+
+ this.$get = function() {
+ return {
+ getConfig: function() {
+ return _config;
+ }
+ };
+ };
+})
+.factory('AuthInterceptor', ['OAuthConfiguration', 'AccessToken', function(OAuthConfiguration, AccessToken) {
+ return {
+ 'request': function(config) {
+ OAuthConfiguration.getConfig().protectedResources.forEach(function(resource) {
+ // If the url is one of the protected resources, we want to see if there's a token and then
+ // add the token if it exists.
+ if (config.url.indexOf(resource) > -1) {
+ var token = AccessToken.get();
+ if (token) {
+ config.headers.Authorization = 'Bearer ' + token.access_token;
+ }
+ }
+ });
+
+ return config;
+ }
+ };
+}]);
\ No newline at end of file
diff --git a/app/scripts/services/oidc-config.js b/app/scripts/services/oidc-config.js
new file mode 100644
index 0000000..f3e73e9
--- /dev/null
+++ b/app/scripts/services/oidc-config.js
@@ -0,0 +1,70 @@
+(function() {
+ 'use strict';
+
+ angular.module('oauth.oidcConfig', [])
+ .factory('OidcConfig', ['Storage', '$http', '$q', '$log', OidcConfig]);
+
+ function OidcConfig(Storage, $http, $q, $log) {
+ var cache = null;
+ return {
+ load: load
+ };
+
+ function load(scope) {
+ if (scope.issuer && scope.wellKnown && scope.wellKnown !== "false") {
+ var promise = loadConfig(scope.issuer);
+ if (scope.wellKnown === "sync") {
+ return promise;
+ }
+ }
+ return $q.when(1);
+ }
+
+ function loadConfig(iss) {
+ if (cache === null) {
+ cache = Storage.get('oidcConfig');
+ }
+ if (angular.isDefined(cache)) {
+ return $q.when(cache);
+ } else {
+ return loadOpenidConfiguration(iss)
+ .then(saveCache)
+ .then(loadJwks)
+ .then(saveCache, errorLogger);
+ }
+ }
+
+ function errorLogger(err) {
+ $log.error("Could not load OIDC config:", err);
+ return $q.reject(err);
+ }
+
+ function saveCache(o) {
+ Storage.set('oidcConfig', cache);
+ return o;
+ }
+
+ function joinPath(x,y) {
+ return x + (x.charAt(x.length - 1) === '/' ? '' : '/') + y;
+ }
+
+ function loadOpenidConfiguration(iss) {
+ var configUri = joinPath(iss, ".well-known/openid-configuration");
+ return $http.get(configUri).then(function(res) {
+ return cache = res.data;
+ }, function(err) {
+ return $q.reject("Could not get config info from " + configUri + ' . Check the availability of this url.');
+ });
+ }
+
+ function loadJwks(oidcConf) {
+ if (oidcConf.jwks_uri) {
+ return $http.get(oidcConf.jwks_uri).then(function(res) {
+ return oidcConf.jwks = res.data;
+ });
+ } else {
+ return $q.reject("No jwks_uri found.");
+ }
+ }
+ }
+})();
diff --git a/app/scripts/services/profile.js b/app/scripts/services/profile.js
index 671136a..4d257fc 100644
--- a/app/scripts/services/profile.js
+++ b/app/scripts/services/profile.js
@@ -1,18 +1,21 @@
'use strict';
-var profileClient = angular.module('oauth.profile', [])
+var profileClient = angular.module('oauth.profile', []);
-profileClient.factory('Profile', function($http, AccessToken) {
+profileClient.factory('Profile', ['$http', 'AccessToken', '$rootScope', function($http, AccessToken, $rootScope) {
var service = {};
var profile;
service.find = function(uri) {
var promise = $http.get(uri, { headers: headers() });
- promise.success(function(response) { profile = response });
+ promise.then(function(response) {
+ profile = response.data;
+ $rootScope.$broadcast('oauth:profile', profile);
+ });
return promise;
};
- service.get = function(uri) {
+ service.get = function() {
return profile;
};
@@ -26,4 +29,4 @@ profileClient.factory('Profile', function($http, AccessToken) {
};
return service;
-});
+}]);
diff --git a/app/scripts/services/storage.js b/app/scripts/services/storage.js
new file mode 100644
index 0000000..d2d298c
--- /dev/null
+++ b/app/scripts/services/storage.js
@@ -0,0 +1,49 @@
+'use strict';
+
+var storageService = angular.module('oauth.storage', ['ngStorage']);
+
+storageService.factory('Storage', ['$rootScope', '$sessionStorage', '$localStorage', function($rootScope, $sessionStorage, $localStorage){
+
+ var service = {
+ storage: $sessionStorage // By default
+ };
+
+ /**
+ * Deletes the item from storage,
+ * Returns the item's previous value
+ */
+ service.delete = function (name) {
+ var stored = this.get(name);
+ delete this.storage[name];
+ return stored;
+ };
+
+ /**
+ * Returns the item from storage
+ */
+ service.get = function (name) {
+ return this.storage[name];
+ };
+
+ /**
+ * Sets the item in storage to the value specified
+ * Returns the item's value
+ */
+ service.set = function (name, value) {
+ this.storage[name] = value;
+ return this.get(name);
+ };
+
+ /**
+ * Change the storage service being used
+ */
+ service.use = function (storage) {
+ if (storage === 'sessionStorage') {
+ this.storage = $sessionStorage;
+ } else if (storage === 'localStorage') {
+ this.storage = $localStorage;
+ }
+ };
+
+ return service;
+}]);
\ No newline at end of file
diff --git a/bower.json b/bower.json
index 124f89e..c690710 100644
--- a/bower.json
+++ b/bower.json
@@ -1,6 +1,6 @@
{
"name": "oauth-ng",
- "version": "0.2.7",
+ "version": "0.4.10",
"main": [
"dist/oauth-ng.js",
"dist/views/templates/default"
@@ -14,8 +14,9 @@
"package.json"
],
"dependencies": {
- "angular": "~1.2.22",
- "ngstorage": "~0.3.0"
+ "angular": "~1.3.12",
+ "ngstorage": "~0.3.0",
+ "jsrsasign": "~5.0.5"
},
"devDependencies": {
"json3": "~3.2.4",
@@ -23,9 +24,9 @@
"timecop": "~0.1.1",
"bootstrap-sass": "~3.0.2",
"jquery": "~1.9.1",
- "angular-mocks": "~1.2.22"
+ "angular-mocks": "~1.3.12"
},
"resolutions": {
- "angular": "~1.2.22"
+ "angular": "1.3.20"
}
}
diff --git a/dist/oauth-ng.js b/dist/oauth-ng.js
index e930a88..3ad2269 100644
--- a/dist/oauth-ng.js
+++ b/dist/oauth-ng.js
@@ -1,252 +1,697 @@
-/* oauth-ng - v0.2.7 - 2014-08-26 */
+/* oauth-ng - v0.4.10 - 2016-05-25 */
'use strict';
// App libraries
-var app = angular.module('oauth', [
+angular.module('oauth', [
'oauth.directive', // login directive
+ 'oauth.idToken', // id token service (only for OpenID Connect)
+ 'oauth.oidcConfig', // for loading OIDC configuration from .well-known/openid-configuration endpoint
'oauth.accessToken', // access token service
'oauth.endpoint', // oauth endpoint service
'oauth.profile', // profile model
- 'oauth.interceptor' // bearer token interceptor
+ 'oauth.storage', // storage
+ 'oauth.interceptor', // bearer token interceptor
+ 'oauth.configuration' // token appender
])
-
-angular.module('oauth').config(['$locationProvider','$httpProvider',
+ .config(['$locationProvider','$httpProvider',
function($locationProvider, $httpProvider) {
$httpProvider.interceptors.push('ExpiredInterceptor');
}]);
'use strict';
-var accessTokenService = angular.module('oauth.accessToken', ['ngStorage']);
-
-accessTokenService.factory('AccessToken', function($rootScope, $location, $sessionStorage, $timeout) {
+var idTokenService = angular.module('oauth.idToken', []);
- var service = {};
- var token = null;
+idTokenService.factory('IdToken', ['Storage', function(Storage) {
+ var service = {
+ issuer: null,
+ subject: null,
+ //clientId, should match 'aud' claim
+ clientId: null,
+ /*
+ The public key to verify the signature, supports:
+ 1.RSA public key in PEM string: e.g. "-----BEGIN PUBLIC KEY..."
+ 2.X509 certificate in PEM string: e.g. "-----BEGIN CERTIFICATE..."
+ 3.JWK (Json Web Key): e.g. {kty: "RSA", n: "0vx7...", e: "AQAB"}
- /*
- * Returns the access token.
+ If not set, the id_token header should carry the key or the url to retrieve the key
+ */
+ pubKey: null
+ };
+ /**
+ * OidcException
+ * @param {string } message - The exception error message
+ * @constructor
*/
-
- service.get = function() {
- return token
+ function OidcException(message) {
+ this.name = 'OidcException';
+ this.message = message;
}
+ OidcException.prototype = new Error();
+ OidcException.prototype.constructor = OidcException;
+ /**
+ * For initialization
+ * @param scope
+ */
+ service.set = function(scope) {
+ this.issuer = scope.issuer;
+ this.subject = scope.subject;
+ this.clientId = scope.clientId;
+ this.pubKey = scope.pubKey;
+ };
- /*
- * 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
+ /**
+ * Validate id_token and access_token(if there's one)
+ * If validation passes, the id_token payload(claims) will be populated to 'params'
+ * Otherwise error will set to 'params' and tokens will be removed
+ *
+ * @param params
*/
+ service.validateTokensAndPopulateClaims = function(params) {
+ var valid = false;
+ var message = '';
+ try {
+ valid = this.validateIdToken(params.id_token);
+ /*
+ if response_type is 'id_token token', then we will get both id_token and access_token,
+ access_token needs to be validated as well
+ */
+ if (valid && params.access_token) {
+ valid = this.validateAccessToken(params.id_token, params.access_token);
+ }
+ } catch (error) {
+ message = error.message;
+ }
- service.set = function() {
- service.setTokenFromString($location.hash());
- service.setTokenFromSession();
- return token
- }
+ if (valid) {
+ params.id_token_claims = getIdTokenPayload(params.id_token);
+ } else {
+ params.id_token = null;
+ params.access_token = null;
+ params.error = 'Failed to validate token:' + message;
+ }
+ };
- /*
- * Delete the access token and remove the session.
+ /**
+ * Validates the id_token
+ * @param {String} idToken The id_token
+ * @returns {boolean} True if all the check passes, False otherwise
*/
+ service.validateIdToken = function(idToken) {
+ return this.verifyIdTokenSig(idToken) && this.verifyIdTokenInfo(idToken);
+ };
- service.destroy = function() {
- delete $sessionStorage.token;
- return token = null;
- }
-
+ /**
+ * Validate access_token based on the 'alg' and 'at_hash' value of the id_token header
+ * per spec: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitTokenValidation
+ *
+ * @param idToken The id_token
+ * @param accessToken The access_token
+ * @returns {boolean} true if validation passes
+ */
+ service.validateAccessToken = function(idToken, accessToken) {
+ var header = getJsonObject(getIdTokenParts(idToken)[0]);
+ if (header.at_hash) {
+ var shalevel = header.alg.substr(2);
+ if (shalevel !== '256' && shalevel !== '384' && shalevel !== '512') {
+ throw new OidcException('Unsupported hash algorithm, expecting sha256, sha384, or sha512');
+ }
+ var md = new KJUR.crypto.MessageDigest({'alg':'sha'+ shalevel, 'prov':'cryptojs'});
+ //hex representation of the hash
+ var hexStr = md.digestString(accessToken);
+ //take first 128bits and base64url encoding it
+ var expected = hextob64u(hexStr.substring(0, 32));
+
+ return expected === header.at_hash;
+ } else {
+ return true;
+ }
+ };
- /*
- * Tells if the access token is expired.
+ /**
+ * Verifies the ID Token signature using the specified public key
+ * The id_token header can optionally carry public key or the url to retrieve the public key
+ * Otherwise will use the public key configured using 'pubKey'
+ *
+ * Supports only RSA signatures ['RS256', 'RS384', 'RS512']
+ * @param {string}idToken The ID Token string
+ * @returns {boolean} Indicates whether the signature is valid or not
+ * @throws {OidcException}
*/
+ service.verifyIdTokenSig = function (idToken) {
- service.expired = function() {
- return (token && token.expires_at && token.expires_at < new Date());
- }
+ var idtParts = getIdTokenParts(idToken);
+ var header = getJsonObject(idtParts[0]);
+ if(!header.alg || header.alg.substr(0, 2) !== 'RS') {
+ throw new OidcException('Unsupported JWS signature algorithm ' + header.alg);
+ }
+ var matchedPubKey = null;
- /* * * * * * * * * *
- * PRIVATE METHODS *
- * * * * * * * * * */
+ if (header.jwk) {
+ //Take the JWK if it comes with the id_token
+ matchedPubKey = header.jwk;
+ if (matchedPubKey.kid && header.kid && matchedPubKey.kid !== header.kid) {
+ throw new OidcException('Json Web Key ID not match');
+ }
+ /*
+ TODO: Support for "jku" (JWK Set URL), "x5u" (X.509 URL), "x5c" (X.509 Certificate Chain) parameter to get key
+ per http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-26#page-9
+ */
+ } else {
+ //Try to load the key from .well-known configuration
+ var oidcConfig = Storage.get('oidcConfig');
+ if (angular.isDefined(oidcConfig) && oidcConfig.jwks && oidcConfig.jwks.keys) {
+ oidcConfig.jwks.keys.forEach(function(key) {
+ if (key.kid === header.kid) {
+ matchedPubKey = key;
+ }
+ });
+ } else {
+ //Use configured public key
+ var jwk = getJsonObject(this.pubKey);
+ matchedPubKey = jwk ? jwk : this.pubKey; //JWK or PEM
+ }
+ }
+ if(!matchedPubKey) {
+ throw new OidcException('No public key found to verify signature');
+ }
- /*
- * Get the access token from a string and save it
+ return rsaVerifyJWS(idToken, matchedPubKey, header.alg);
+ };
+
+ /**
+ * Validates the information in the ID Token against configuration
+ * @param {string} idtoken The ID Token string
+ * @returns {boolean} Validity of the ID Token
+ * @throws {OidcException}
*/
+ service.verifyIdTokenInfo = function(idtoken) {
+ var valid = false;
+
+ if (idtoken) {
+ var idtParts = getIdTokenParts(idtoken);
+ var payload = getJsonObject(idtParts[1]);
+ if (payload) {
+ var now = new Date() / 1000;
+ if (payload.iat > now + 60)
+ throw new OidcException('ID Token issued time is later than current time');
+
+ if (payload.exp < now )
+ throw new OidcException('ID Token expired');
+
+ if (now < payload.ntb)
+ throw new OidcException('ID Token is invalid before '+ payload.ntb);
+
+ if (payload.iss && this.issuer && payload.iss !== this.issuer)
+ throw new OidcException('Invalid issuer ' + payload.iss + ' != ' + this.issuer);
+
+ if (payload.sub && this.subject && payload.sub !== this.subject)
+ throw new OidcException('Invalid subject ' + payload.sub + ' != ' + this.subject);
+
+ if (payload.aud) {
+ if (payload.aud instanceof Array && !KJUR.jws.JWS.inArray(this.clientId, payload.aud)) {
+ throw new OidcException('Client not in intended audience:' + payload.aud);
+ }
+ if (typeof payload.aud === 'string' && payload.aud !== this.clientId) {
+ throw new OidcException('Invalid audience ' + payload.aud + ' != ' + this.clientId);
+ }
+ }
+
+ //TODO: nonce support ? probably need to redo current nonce support
+ //if(payload['nonce'] != sessionStorage['nonce'])
+ // throw new OidcException('invalid nonce');
+ valid = true;
+ } else
+ throw new OidcException('Unable to parse JWS payload');
+ }
+ return valid;
+ };
- service.setTokenFromString = function(hash) {
- var token = getTokenFromString(hash);
+ /**
+ * Verifies the JWS string using the JWK
+ * @param {string} jws The JWS string
+ * @param {object} pubKey The public key that will be used to verify the signature
+ * @param {string} alg The algorithm string. Expecting 'RS256', 'RS384', or 'RS512'
+ * @returns {boolean} Validity of the JWS signature
+ * @throws {OidcException}
+ */
+ var rsaVerifyJWS = function (jws, pubKey, alg) {
+ /*
+ convert various public key format to RSAKey object
+ see @KEYUTIL.getKey for a full list of supported input format
+ */
+ var rsaKey = KEYUTIL.getKey(pubKey);
+
+ return KJUR.jws.JWS.verify(jws, rsaKey, [alg]);
+ };
- if (token) {
- removeFragment();
- service.setToken(token);
- setExpiresAt(token);
- $rootScope.$broadcast('oauth:login', token);
- }
+ /**
+ * Splits the ID Token string into the individual JWS parts
+ * @param {string} id_token ID Token
+ * @returns {Array} An array of the JWS compact serialization components (header, payload, signature)
+ */
+ var getIdTokenParts = function (id_token) {
+ var jws = new KJUR.jws.JWS();
+ jws.parseJWS(id_token);
+ return [jws.parsedJWS.headS, jws.parsedJWS.payloadS, jws.parsedJWS.si];
};
+ /**
+ * Get the contents of the ID Token payload as an JSON object
+ * @param {string} id_token ID Token
+ * @returns {object} The ID Token payload JSON object
+ */
+ var getIdTokenPayload = function (id_token) {
+ var parts = getIdTokenParts(id_token);
+ if(parts)
+ return getJsonObject(parts[1]);
+ };
- /*
- * Parse the fragment URI and return an object
+ /**
+ * Get the JSON object from the JSON string
+ * @param {string} jsonS JSON string
+ * @returns {object|null} JSON object or null
*/
+ var getJsonObject = function (jsonS) {
+ var jws = KJUR.jws.JWS;
+ if(jws.isSafeJSONString(jsonS)) {
+ return jws.readSafeJSONString(jsonS);
+ }
+ return null;
+ };
+
+ return service;
- var getTokenFromString = function(hash) {
- var splitted = hash.split('&');
- var params = {};
+}]);
- for (var i = 0; i < splitted.length; i++) {
- var param = splitted[i].split('=');
- var key = param[0];
- var value = param[1];
- params[key] = value
+(function() {
+ 'use strict';
+
+ angular.module('oauth.oidcConfig', [])
+ .factory('OidcConfig', ['Storage', '$http', '$q', '$log', OidcConfig]);
+
+ function OidcConfig(Storage, $http, $q, $log) {
+ var cache = null;
+ return {
+ load: load
+ };
+
+ function load(scope) {
+ if (scope.issuer && scope.wellKnown && scope.wellKnown !== "false") {
+ var promise = loadConfig(scope.issuer);
+ if (scope.wellKnown === "sync") {
+ return promise;
+ }
+ }
+ return $q.when(1);
}
- if (params.access_token || params.error)
- return params;
+ function loadConfig(iss) {
+ if (cache === null) {
+ cache = Storage.get('oidcConfig');
+ }
+ if (angular.isDefined(cache)) {
+ return $q.when(cache);
+ } else {
+ return loadOpenidConfiguration(iss)
+ .then(saveCache)
+ .then(loadJwks)
+ .then(saveCache, errorLogger);
+ }
+ }
+
+ function errorLogger(err) {
+ $log.error("Could not load OIDC config:", err);
+ return $q.reject(err);
+ }
+
+ function saveCache(o) {
+ Storage.set('oidcConfig', cache);
+ return o;
+ }
+
+ function joinPath(x,y) {
+ return x + (x.charAt(x.length - 1) === '/' ? '' : '/') + y;
+ }
+
+ function loadOpenidConfiguration(iss) {
+ var configUri = joinPath(iss, ".well-known/openid-configuration");
+ return $http.get(configUri).then(function(res) {
+ return cache = res.data;
+ }, function(err) {
+ return $q.reject("Could not get config info from " + configUri + ' . Check the availability of this url.');
+ });
+ }
+
+ function loadJwks(oidcConf) {
+ if (oidcConf.jwks_uri) {
+ return $http.get(oidcConf.jwks_uri).then(function(res) {
+ return oidcConf.jwks = res.data;
+ });
+ } else {
+ return $q.reject("No jwks_uri found.");
+ }
+ }
}
+})();
+
+'use strict';
+var accessTokenService = angular.module('oauth.accessToken', []);
- /*
- * Set the access token from the sessionStorage.
+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());
- service.setTokenFromSession = function() {
- if ($sessionStorage.token) {
- var params = $sessionStorage.token;
- params.expires_at = new Date(params.expires_at);
- service.setToken(params);
+ //If hash is present in URL always use it, cuz its coming from oAuth2 provider redirect
+ if(null === service.token){
+ setTokenFromSession();
}
- }
+ return this.token;
+ };
- /*
- * Save the access token into the session
+ /**
+ * Delete the access token and remove the session.
+ * @returns {null}
*/
+ service.destroy = function(){
+ Storage.delete('token');
+ this.token = null;
+ return this.token;
+ };
- var setTokenInSession = function() {
- $sessionStorage.token = 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);
- /*
- * Set the access token.
+ 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);
+ }
+ };
- service.setToken = function(params) {
- token = token || {}; // init the token
- angular.extend(token, params); // set the access token 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 token;
+ 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 access token expiration date (useful for refresh logics)
+ 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);
+ };
- var setExpiresAt = function(token) {
- if (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(token.expires_in) - 60); // 60 seconds less to secure browser and response latency
- token.expires_at = expires_at;
+ 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 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);
+ }
+ };
- var setExpiresAtEvent = function() {
- var time = (new Date(token.expires_at)) - (new Date())
- if (time) { $timeout(function() { $rootScope.$broadcast('oauth:expired', token) }, time) }
- }
-
+ var cancelExpiresAtEvent = function() {
+ if(expiresAtEvent) {
+ $timeout.cancel(expiresAtEvent);
+ expiresAtEvent = undefined;
+ }
+ };
- /*
- * Remove the fragment URI
- * TODO we need to remove only the access token
+ /**
+ * 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,'');
+ });
- var removeFragment = function(scope) {
- $location.hash('');
- }
-
+ $location.hash(curHash);
+ };
return service;
-});
+
+}]);
'use strict';
var endpointClient = angular.module('oauth.endpoint', []);
-endpointClient.factory('Endpoint', function(AccessToken, $location) {
+endpointClient.factory('Endpoint', ['$rootScope', 'AccessToken', '$q', '$http', function($rootScope, AccessToken, $q, $http) {
var service = {};
- var url;
+ var buildOauthUrl = function (path, params) {
+ var oAuthScope = (params.scope) ? encodeURIComponent(params.scope) : '',
+ state = (params.state) ? encodeURIComponent(params.state) : '',
+ authPathHasQuery = (params.authorizePath.indexOf('?') == -1) ? false : true,
+ appendChar = (authPathHasQuery) ? '&' : '?', //if authorizePath has ? already append OAuth2 params
+ nonceParam = (params.nonce) ? '&nonce=' + params.nonce : '',
+ responseType = encodeURIComponent(params.responseType);
+
+ return params.site +
+ path +
+ appendChar + 'response_type=' + responseType + '&' +
+ 'client_id=' + encodeURIComponent(params.clientId) + '&' +
+ 'redirect_uri=' + encodeURIComponent(params.redirectUri) + '&' +
+ 'scope=' + oAuthScope + '&' +
+ 'state=' + state + nonceParam;
+ };
+
+ var extendValidity = function (tokenInfo) {
+ AccessToken.updateExpiry(tokenInfo.expires);
+ };
/*
* Defines the authorization URL
*/
- service.set = function(scope) {
- var oAuthScope = (scope.scope) ? scope.scope : '',
- state = (scope.state) ? encodeURIComponent(scope.state) : '',
- authPathHasQuery = (scope.authorizePath.indexOf('?') == -1) ? false : true,
- appendChar = (authPathHasQuery) ? '&' : '?'; //if authorizePath has ? already append OAuth2 params
+ service.set = function(configuration) {
+ this.config = configuration;
+ return this.get();
+ };
- url = scope.site +
- scope.authorizePath +
- appendChar + 'response_type=token&' +
- 'client_id=' + encodeURIComponent(scope.clientId) + '&' +
- 'redirect_uri=' + encodeURIComponent(scope.redirectUri) + '&' +
- 'scope=' + oAuthScope + '&' +
- 'state=' + state;
+ /*
+ * Returns the authorization URL
+ */
- return url;
+ service.get = function(overrides) {
+ var params = angular.extend( {}, service.config, overrides);
+ return buildOauthUrl(params.authorizePath, params);
};
/*
- * Returns the authorization URL
+ * Redirects the app to the authorization URL
*/
- service.get = function() {
- return url;
+ service.redirect = function(overrides) {
+ var targetLocation = this.get(overrides);
+ $rootScope.$broadcast('oauth:logging-in');
+ window.location.replace(targetLocation);
};
+ /*
+ * Alias for 'redirect'
+ */
+ service.login = function() {
+ service.redirect();
+ };
/*
- * Redirects the app to the authorization URL
+ * Check the validity of the token if a session path is available
+ */
+ service.checkValidity = function() {
+ var params = service.config;
+ if( params.sessionPath ) {
+ var token = AccessToken.get();
+ if( !token ) {
+ return $q.reject("No token configured");
+ }
+ var path = params.site + params.sessionPath + "?token=" + token.access_token;
+ return $http.get(path).then( function(httpResponse) {
+ var tokenInfo = httpResponse.data;
+ if(tokenInfo.valid) {
+ extendValidity(tokenInfo);
+ return true;
+ } else {
+ return $q.reject("Server replied: token is invalid.");
+ }
+ });
+ } else {
+ return $q.reject("You must give a :session-path param in order to validate the token.")
+ }
+ };
+
+ /*
+ * Destroys the session, sends the user to the logout url if set.
+ * First broadcasts 'logging-out' and then 'logout' when finished.
*/
- service.redirect = function() {
- window.location.replace(url);
+ service.logout = function() {
+ var params = service.config;
+ AccessToken.destroy();
+ $rootScope.$broadcast('oauth:logging-out');
+ if( params.logoutPath ) {
+ window.location.replace(buildOauthUrl(params.logoutPath, params));
+ }
+ $rootScope.$broadcast('oauth:logout');
};
return service;
-});
+}]);
'use strict';
-var profileClient = angular.module('oauth.profile', [])
+var profileClient = angular.module('oauth.profile', []);
-profileClient.factory('Profile', function($http, AccessToken) {
+profileClient.factory('Profile', ['$http', 'AccessToken', '$rootScope', function($http, AccessToken, $rootScope) {
var service = {};
var profile;
service.find = function(uri) {
var promise = $http.get(uri, { headers: headers() });
- promise.success(function(response) { profile = response });
+ promise.then(function(response) {
+ profile = response.data;
+ $rootScope.$broadcast('oauth:profile', profile);
+ });
return promise;
};
- service.get = function(uri) {
+ service.get = function() {
return profile;
};
@@ -260,141 +705,289 @@ profileClient.factory('Profile', function($http, AccessToken) {
};
return service;
-});
+}]);
'use strict';
-var interceptorService = angular.module('oauth.interceptor', []);
+var storageService = angular.module('oauth.storage', ['ngStorage']);
-interceptorService.factory('ExpiredInterceptor', function ($rootScope, $q, $sessionStorage) {
+storageService.factory('Storage', ['$rootScope', '$sessionStorage', '$localStorage', function($rootScope, $sessionStorage, $localStorage){
- var service = {};
+ var service = {
+ storage: $sessionStorage // By default
+ };
- service.request = function(config) {
- var token = $sessionStorage.token;
+ /**
+ * Deletes the item from storage,
+ * Returns the item's previous value
+ */
+ service.delete = function (name) {
+ var stored = this.get(name);
+ delete this.storage[name];
+ return stored;
+ };
- if (token && expired(token))
- $rootScope.$broadcast('oauth:expired', token);
+ /**
+ * Returns the item from storage
+ */
+ service.get = function (name) {
+ return this.storage[name];
+ };
- return config;
+ /**
+ * Sets the item in storage to the value specified
+ * Returns the item's value
+ */
+ service.set = function (name, value) {
+ this.storage[name] = value;
+ return this.get(name);
};
- var expired = function(token) {
- return (token && token.expires_at && new Date(token.expires_at) < new Date())
+ /**
+ * Change the storage service being used
+ */
+ service.use = function (storage) {
+ if (storage === 'sessionStorage') {
+ this.storage = $sessionStorage;
+ } else if (storage === 'localStorage') {
+ this.storage = $localStorage;
+ }
};
return service;
-});
+}]);
+'use strict';
+var oauthConfigurationService = angular.module('oauth.configuration', []);
+
+oauthConfigurationService.provider('OAuthConfiguration', function() {
+ var _config = {};
+
+ this.init = function(config, httpProvider) {
+ _config.protectedResources = config.protectedResources || [];
+ httpProvider.interceptors.push('AuthInterceptor');
+ };
+
+ this.$get = function() {
+ return {
+ getConfig: function() {
+ return _config;
+ }
+ };
+ };
+})
+.factory('AuthInterceptor', ['OAuthConfiguration', 'AccessToken', function(OAuthConfiguration, AccessToken) {
+ return {
+ 'request': function(config) {
+ OAuthConfiguration.getConfig().protectedResources.forEach(function(resource) {
+ // If the url is one of the protected resources, we want to see if there's a token and then
+ // add the token if it exists.
+ if (config.url.indexOf(resource) > -1) {
+ var token = AccessToken.get();
+ if (token) {
+ config.headers.Authorization = 'Bearer ' + token.access_token;
+ }
+ }
+ });
+
+ return config;
+ }
+ };
+}]);
'use strict';
-var directives = angular.module('oauth.directive', []);
+var interceptorService = angular.module('oauth.interceptor', []);
-directives.directive('oauth', function(AccessToken, Endpoint, Profile, $location, $rootScope, $compile, $http, $templateCache) {
-
- var definition = {
- 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
- 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
- }
- };
+interceptorService.factory('ExpiredInterceptor', ['Storage', '$rootScope', function (Storage, $rootScope) {
- definition.link = function postLink(scope, element, attrs) {
- scope.show = 'none';
+ var service = {};
- scope.$watch('clientId', function(value) { init() });
+ service.request = function(config) {
+ var token = Storage.get('token');
- var init = function() {
- initAttributes(); // sets defaults
- compile(); // compiles the desired layout
- Endpoint.set(scope); // sets the oauth authorization url
- 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)
- };
+ if (token && expired(token)) {
+ $rootScope.$broadcast('oauth:expired', 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.text = scope.text || 'Sign In';
- scope.state = scope.state || undefined;
- scope.scope = scope.scope || undefined;
- };
+ return config;
+ };
- var compile = function() {
- $http.get(scope.template, { cache: $templateCache }).success(function(html) {
- element.html(html);
- $compile(element.contents())(scope);
- });
- };
+ var expired = function(token) {
+ return (token && token.expires_at && new Date(token.expires_at) < new Date());
+ };
- var initProfile = function(scope) {
- var token = AccessToken.get();
+ return service;
+}]);
- if (token && token.access_token && scope.profileUri) {
- Profile.find(scope.profileUri).success(function(response) {
- scope.profile = response
- })
- }
- };
+'use strict';
- var initView = function() {
- var token = AccessToken.get();
+var directives = angular.module('oauth.directive', []);
- if (!token) { return loggedOut() } // without access token it's logged out
- if (token.access_token) { return authorized() } // if there is the access token we are done
- if (token.error) { return denied() } // if the request has been denied we fire the denied event
+directives.directive('oauth', [
+ 'IdToken',
+ 'AccessToken',
+ 'Endpoint',
+ 'Profile',
+ 'Storage',
+ 'OidcConfig',
+ '$location',
+ '$rootScope',
+ '$compile',
+ '$http',
+ '$templateCache',
+ '$timeout',
+ function(IdToken, AccessToken, Endpoint, Profile, Storage, OidcConfig, $location, $rootScope, $compile, $http, $templateCache, $timeout) {
+
+ var definition = {
+ 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.
+ }
};
- scope.login = function() {
- Endpoint.redirect();
- };
+ definition.link = function postLink(scope, element) {
+ scope.show = 'none';
- scope.logout = function() {
- AccessToken.destroy(scope);
- loggedOut();
- };
+ scope.$watch('clientId', function() {
+ init();
+ });
- // user is authorized
- var authorized = function() {
- $rootScope.$broadcast('oauth:authorized', AccessToken.get());
- scope.show = 'logged-in';
- };
+ var init = function() {
+ initAttributes(); // sets defaults
+ Storage.use(scope.storage);// set storage
+ compile(); // compiles the desired layout
+ Endpoint.set(scope); // sets the oauth authorization url
+ 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
+ });
+ };
+
+ 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';
+ };
+
+ var compile = function() {
+ $http.get(scope.template, { cache: $templateCache }).then(function(html) {
+ element.html(html.data);
+ $compile(element.contents())(scope);
+ });
+ };
+
+ var initProfile = function(scope) {
+ var token = AccessToken.get();
+
+ if (token && token.access_token && scope.profileUri) {
+ Profile.find(scope.profileUri).then(function(response) {
+ scope.profile = response.data;
+ });
+ }
+ };
+
+ var initView = function () {
+ var token = AccessToken.get();
+
+ if (!token) {
+ return scope.login();
+ } // without access token it's logged out, so we attempt to log in
+ if (AccessToken.expired()) {
+ return expired();
+ } // with a token, but it's expired
+ if (token.access_token) {
+ return authorized();
+ } // if there is the access token we are done
+ if (token.error) {
+ return denied();
+ } // if the request has been denied we fire the denied event
+ };
+
+ scope.login = function () {
+ Endpoint.redirect();
+ };
+
+ scope.logout = function () {
+ Endpoint.logout();
+ $rootScope.$broadcast('oauth:loggedOut');
+ scope.show = 'logged-out';
+ };
+
+ scope.$on('oauth:expired',expired);
+
+ // user is authorized
+ var authorized = function() {
+ $rootScope.$broadcast('oauth:authorized', AccessToken.get());
+ scope.show = 'logged-in';
+ };
+
+ var expired = function() {
+ $rootScope.$broadcast('oauth:expired');
+ scope.logout();
+ };
+
+ // set the oauth directive to the denied status
+ var denied = function() {
+ scope.show = 'denied';
+ $rootScope.$broadcast('oauth:denied');
+ };
+
+ var checkValidity = function() {
+ Endpoint.checkValidity().then(function() {
+ $rootScope.$broadcast('oauth:valid');
+ }).catch(function(message){
+ $rootScope.$broadcast('oauth:invalid', message);
+ });
+ };
+
+ var refreshDirective = function () {
+ scope.$apply();
+ };
+
+ // Updates the template at runtime
+ scope.$on('oauth:template:update', function(event, template) {
+ scope.template = template;
+ compile(scope);
+ });
- // set the oauth directive to the logged-out status
- var loggedOut = function() {
- $rootScope.$broadcast('oauth:logout');
- scope.show = 'logged-out';
- };
+ // Hack to update the directive content on logout
+ scope.$on('$routeChangeSuccess', function () {
+ $timeout(refreshDirective);
+ });
- // set the oauth directive to the denied status
- var denied = function() {
- scope.show = 'denied';
- $rootScope.$broadcast('oauth:denied');
+ scope.$on('$stateChangeSuccess', function () {
+ $timeout(refreshDirective);
+ });
};
- // Updates the template at runtime
- scope.$on('oauth:template:update', function(event, template) {
- scope.template = template;
- compile(scope);
- });
-
- // Hack to update the directive content on logout
- // TODO think to a cleaner solution
- scope.$on('$routeChangeSuccess', function () {
- init();
- });
- };
-
- return definition
-});
+ return definition;
+ }
+]);
diff --git a/karma.conf.js b/karma.conf.js
index b66d4e9..5495d37 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -16,6 +16,7 @@ module.exports = function(config) {
'app/bower_components/angular-mocks/angular-mocks.js',
'app/bower_components/ngstorage/ngStorage.js',
'app/bower_components/timecop/timecop-0.1.1.js',
+ 'app/bower_components/jsrsasign/jsrsasign-latest-all-min.js',
'app/scripts/*.js',
'app/scripts/**/*.js',
'app/views/**/*.html',
diff --git a/package.json b/package.json
index 72e070d..8dea385 100644
--- a/package.json
+++ b/package.json
@@ -1,17 +1,19 @@
{
"name": "oauth-ng",
- "version": "0.2.7",
+ "version": "0.4.10",
"author": "Andrea Reginato ",
"description": "AngularJS Directive for OAuth 2.0",
"repository": {
"type": "git",
- "url": "https://github.com/andreareginato/oauth-ng"
+ "url": "https://github.com/angularjs-oauth/oauth-ng"
},
"dependencies": {},
"devDependencies": {
+ "bower": "^1.3.12",
"grunt": "~0.4.1",
"grunt-autoprefixer": "~0.4.0",
"grunt-bower-install": "~0.7.0",
+ "grunt-cli": "^0.1.13",
"grunt-concurrent": "~0.4.1",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-coffee": "~0.7.0",
@@ -26,30 +28,32 @@
"grunt-contrib-uglify": "~0.2.0",
"grunt-contrib-watch": "~0.5.2",
"grunt-google-cdn": "~0.2.0",
+ "grunt-karma": "~0.8.0",
"grunt-newer": "~0.5.4",
"grunt-ngmin": "~0.0.2",
+ "grunt-protractor-runner": "~0.2.4",
+ "grunt-replace": "~0.7.7",
"grunt-rev": "~0.1.0",
+ "grunt-string-replace": "~0.2.7",
"grunt-svgmin": "~0.2.0",
"grunt-usemin": "~2.0.0",
"jshint-stylish": "~0.1.3",
- "load-grunt-tasks": "~0.2.0",
- "time-grunt": "~0.2.1",
- "karma-ng-scenario": "~0.1.0",
- "grunt-karma": "~0.8.0",
"karma": "~0.12.0",
- "karma-ng-html2js-preprocessor": "~0.1.0",
- "karma-jasmine": "~0.2.2",
- "karma-phantomjs-launcher": "~0.1.2",
"karma-chrome-launcher": "~0.1.2",
"karma-coverage": "~0.2.1",
- "grunt-protractor-runner": "~0.2.4",
- "grunt-replace": "~0.7.7",
- "grunt-string-replace": "~0.2.7"
+ "karma-jasmine": "~0.2.2",
+ "karma-ng-html2js-preprocessor": "~0.1.0",
+ "karma-ng-scenario": "~0.1.0",
+ "karma-phantomjs-launcher": "~0.1.2",
+ "load-grunt-tasks": "~0.2.0",
+ "time-grunt": "~0.2.1"
},
"engines": {
"node": ">=0.8.0"
},
"scripts": {
- "test": "grunt test:unit"
- }
+ "clean": "rm -rf node_modules dist app/bower_components",
+ "test": "grunt test && grunt build"
+ },
+ "main": "dist/oauth-ng.js"
}
diff --git a/test/.jshintrc b/test/.jshintrc
new file mode 100644
index 0000000..847dc88
--- /dev/null
+++ b/test/.jshintrc
@@ -0,0 +1,32 @@
+{
+ "node": true,
+ "browser": true,
+ "esnext": true,
+ "bitwise": true,
+ "camelcase": false,
+ "curly": true,
+ "eqeqeq": true,
+ "immed": true,
+ "indent": 2,
+ "latedef": true,
+ "newcap": true,
+ "noarg": true,
+ "quotmark": "single",
+ "undef": true,
+ "unused": true,
+ "strict": true,
+ "trailing": true,
+ "smarttabs": true,
+ "globals": {
+ "angular": false,
+ "jasmine": false,
+ "Timecop": false,
+ "spyOn": false,
+ "describe": false,
+ "it": false,
+ "expect": false,
+ "beforeEach": false,
+ "afterEach": false,
+ "inject": false
+ }
+}
\ No newline at end of file
diff --git a/test/spec/directives/oauth.js b/test/spec/directives/oauth.js
index e5e34c8..13da42a 100644
--- a/test/spec/directives/oauth.js
+++ b/test/spec/directives/oauth.js
@@ -2,24 +2,27 @@
describe('oauth', function() {
- var $rootScope, $location, $sessionStorage, $httpBackend, $compile, AccessToken, Endpoint, element, scope, result, callback;
+ var $rootScope, $location, Storage, $httpBackend, $compile, AccessToken, Endpoint, element, scope, result, callback,
+ validCallback, invalidCallback;
- var uri = 'http://example.com/oauth/authorize?response_type=token&client_id=client-id&redirect_uri=http://example.com/redirect&scope=scope&state=/';
var fragment = 'access_token=token&token_type=bearer&expires_in=7200&state=/path';
- var denied = 'error=access_denied&error_description=error';
- var headers = { 'Accept': 'application/json, text/plain, */*', 'Authorization': 'Bearer token' }
- var profile = { id: '1', full_name: 'Alice Wonderland', email: 'alice@example.com' };
+ var denied = 'error=access_denied&error_description=error';
+ var headers = { 'Accept': 'application/json, text/plain, */*', 'Authorization': 'Bearer token' };
+ var profile = { id: '1', full_name: 'Alice Wonderland', email: 'alice@example.com' };
+ var session = { valid: true , expires: ((new Date()).getTime() + 356*24*60*60) };
+ var invalidSession = { valid: false };
+ var invalidProfile = { valid: false };
beforeEach(module('oauth'));
beforeEach(module('templates'));
- beforeEach(inject(function($injector) { $rootScope = $injector.get('$rootScope') }));
- beforeEach(inject(function($injector) { $compile = $injector.get('$compile') }));
- beforeEach(inject(function($injector) { $location = $injector.get('$location') }));
- beforeEach(inject(function($injector) { $sessionStorage = $injector.get('$sessionStorage') }));
- beforeEach(inject(function($injector) { $httpBackend = $injector.get('$httpBackend') }));
- beforeEach(inject(function($injector) { AccessToken = $injector.get('AccessToken') }));
- beforeEach(inject(function($injector) { Endpoint = $injector.get('Endpoint') }));
+ beforeEach(inject(function($injector) { $rootScope = $injector.get('$rootScope'); }));
+ beforeEach(inject(function($injector) { $compile = $injector.get('$compile'); }));
+ beforeEach(inject(function($injector) { $location = $injector.get('$location'); }));
+ beforeEach(inject(function($injector) { Storage = $injector.get('Storage'); }));
+ beforeEach(inject(function($injector) { $httpBackend = $injector.get('$httpBackend'); }));
+ beforeEach(inject(function($injector) { AccessToken = $injector.get('AccessToken'); }));
+ beforeEach(inject(function($injector) { Endpoint = $injector.get('Endpoint'); }));
beforeEach(function() {
element = angular.element(
@@ -28,7 +31,10 @@ describe('oauth', function() {
'client="client-id"' +
'redirect="http://example.com/redirect"' +
'scope="scope"' +
- 'profile-uri="http://example.com/me">Sign In' +
+ 'profile-uri="http://example.com/me"' +
+ 'session-path="/session"' +
+ '>Sign In' +
+ '' +
''
);
});
@@ -37,122 +43,155 @@ describe('oauth', function() {
scope = $rootScope;
$compile(element)(scope);
scope.$digest();
- }
-
+ };
describe('when logged in', function() {
beforeEach(function() {
callback = jasmine.createSpy('callback');
+ validCallback = jasmine.createSpy('validCallback');
+ $rootScope.$on('oauth:valid', validCallback);
});
beforeEach(function() {
$location.hash(fragment);
});
- beforeEach(function() {
- $httpBackend.whenGET('http://example.com/me', headers).respond(profile);
- });
-
- beforeEach(function() {
- $rootScope.$on('oauth:authorized', callback);
- });
-
- beforeEach(function() {
- $rootScope.$on('oauth:login', callback);
- });
-
- beforeEach(function() {
- compile($rootScope, $compile);
- });
-
- it('shows the link "Logout #{profile.email}"', function() {
- $rootScope.$apply();
- $httpBackend.flush();
- result = element.find('.logged-in').text();
- expect(result).toBe('Logout alice@example.com');
- });
-
- it('removes the fragment', function() {
- expect($location.hash()).toBe('');
- });
+ describe('invalid session', function () {
+ beforeEach(function () {
+ invalidCallback = jasmine.createSpy('invalidCallback');
+ $httpBackend.whenGET('http://example.com/me', headers).respond(invalidProfile);
+ $httpBackend.whenGET('http://example.com/session?token=token').respond(invalidSession);
+ $rootScope.$on('oauth:invalid', invalidCallback);
+ });
- it('shows the logout link', function() {
- expect(element.find('.logged-out').attr('class')).toMatch('ng-hide');
- expect(element.find('.logged-in').attr('class')).not.toMatch('ng-hide');
- });
+ beforeEach(function () {
+ compile($rootScope, $compile);
+ });
- it('fires the oauth:login and oauth:authorized event', function() {
- var token = AccessToken.get();
- expect(callback.calls.count()).toBe(2);
+ it('hits the session and is invalid', function () {
+ $httpBackend.flush();
+ expect(invalidCallback.calls.count()).toBe(1);
+ });
});
+ describe('valid session', function () {
- describe('when refreshes the page', function() {
-
- beforeEach(function() {
- callback = jasmine.createSpy('callback');
+ beforeEach(function () {
+ $httpBackend.whenGET('http://example.com/me', headers).respond(profile);
+ $httpBackend.whenGET('http://example.com/session?token=token').respond(session);
});
- beforeEach(function() {
+ beforeEach(function () {
$rootScope.$on('oauth:authorized', callback);
});
- beforeEach(function() {
- $location.path('/');
+ beforeEach(function () {
+ $rootScope.$on('oauth:login', callback);
});
- beforeEach(function() {
+ beforeEach(function () {
compile($rootScope, $compile);
});
- it('keeps being logged in', function() {
+ it('shows the link "Logout #{profile.email}"', function () {
$rootScope.$apply();
$httpBackend.flush();
result = element.find('.logged-in').text();
expect(result).toBe('Logout alice@example.com');
});
- it('shows the logout link', function() {
+ it('removes the fragment', function () {
+ expect($location.hash()).toBe('');
+ });
+
+ it('shows the logout link', function () {
expect(element.find('.logged-out').attr('class')).toMatch('ng-hide');
expect(element.find('.logged-in').attr('class')).not.toMatch('ng-hide');
});
- it('fires the oauth:authorized event', function() {
- var event = jasmine.any(Object);
- var token = AccessToken.get();
- expect(callback).toHaveBeenCalledWith(event, token);
+ it('fires the oauth:login and oauth:authorized event', function () {
+ AccessToken.get();
+ expect(callback.calls.count()).toBe(2);
});
- it('does not fire the oauth:login event', function() {
- var token = AccessToken.get();
- expect(callback.calls.count()).toBe(1);
+ it('hits the session and is valid', function () {
+ $httpBackend.flush();
+ expect(validCallback.calls.count()).toBe(1);
});
- });
- describe('when logs out', function() {
+ describe('when refreshes the page', function () {
- beforeEach(function() {
- callback = jasmine.createSpy('callback');
- });
+ beforeEach(function () {
+ callback = jasmine.createSpy('callback');
+ });
- beforeEach(function() {
- $rootScope.$on('oauth:logout', callback);
- });
+ beforeEach(function () {
+ $rootScope.$on('oauth:authorized', callback);
+ });
- beforeEach(function() {
- element.find('.logged-in').click();
- });
+ beforeEach(function () {
+ $location.path('/');
+ });
+
+ beforeEach(function () {
+ compile($rootScope, $compile);
+ });
- it('shows the login link', function() {
- expect(element.find('.logged-out').attr('class')).not.toMatch('ng-hide');
- expect(element.find('.logged-in').attr('class')).toMatch('ng-hide');
+ it('keeps being logged in', function () {
+ $rootScope.$apply();
+ $httpBackend.flush();
+ result = element.find('.logged-in').text();
+ expect(result).toBe('Logout alice@example.com');
+ });
+
+ it('shows the logout link', function () {
+ expect(element.find('.logged-out').attr('class')).toMatch('ng-hide');
+ expect(element.find('.logged-in').attr('class')).not.toMatch('ng-hide');
+ });
+
+ it('fires the oauth:authorized event', function () {
+ var event = jasmine.any(Object);
+ var token = AccessToken.get();
+ expect(callback).toHaveBeenCalledWith(event, token);
+ });
+
+ it('does not fire the oauth:login event', function () {
+ AccessToken.get();
+ expect(callback.calls.count()).toBe(1);
+ });
});
- it('fires the oauth:logout event', function() {
- var event = jasmine.any(Object);
- expect(callback).toHaveBeenCalledWith(event);
+
+ describe('when logs out', function () {
+
+ beforeEach(function () {
+ callback = jasmine.createSpy('callback');
+ });
+
+ beforeEach(function () {
+ $rootScope.$on('oauth:logout', callback);
+ });
+
+ beforeEach(function () {
+ $rootScope.$on('oauth:loggedOut', callback);
+ });
+
+ beforeEach(function () {
+ element.find('.logged-in').click();
+ });
+
+ it('shows the login link', function () {
+ expect(element.find('.logged-out').attr('class')).not.toMatch('ng-hide');
+ expect(element.find('.logged-in').attr('class')).toMatch('ng-hide');
+ });
+
+ it('fires the oauth:logout and oauth:loggedOut event', function () {
+ var event = jasmine.any(Object);
+ expect(callback).toHaveBeenCalledWith(event);
+ expect(callback.calls.count()).toBe(2);
+ });
});
});
});
@@ -162,10 +201,13 @@ describe('oauth', function() {
beforeEach(function() {
callback = jasmine.createSpy('callback');
+ invalidCallback = jasmine.createSpy('invalidCallback');
+ $httpBackend.whenGET('http://example.com/session?token=token').respond(invalidSession);
+ $rootScope.$on('oauth:invalid', invalidCallback);
});
beforeEach(function() {
- $rootScope.$on('oauth:logout', callback);
+ $rootScope.$on('oauth:loggedOut', callback);
});
beforeEach(function() {
@@ -173,7 +215,7 @@ describe('oauth', function() {
});
beforeEach(function() {
- compile($rootScope, $compile)
+ compile($rootScope, $compile);
});
beforeEach(function() {
@@ -195,10 +237,18 @@ describe('oauth', function() {
expect(element.find('.logged-in').attr('class')).toMatch('ng-hide');
});
- it('fires the oauth:logout event', function() {
+ it('fires the oauth:loggedOut event', function() {
var event = jasmine.any(Object);
expect(callback).toHaveBeenCalledWith(event);
});
+
+ it('does not fire the oauth:logout event', function() {
+ expect(callback.calls.count()).toBe(1);
+ });
+
+ it('fires the oauth:invalid event', function () {
+ expect(invalidCallback.calls.count()).toBe(1);
+ });
});
@@ -206,6 +256,7 @@ describe('oauth', function() {
beforeEach(function() {
callback = jasmine.createSpy('callback');
+ $httpBackend.whenGET('http://example.com/session?token=undefined').respond(invalidSession);
});
beforeEach(function() {
@@ -217,7 +268,7 @@ describe('oauth', function() {
});
beforeEach(function() {
- compile($rootScope, $compile)
+ compile($rootScope, $compile);
});
beforeEach(function() {
@@ -254,7 +305,7 @@ describe('oauth', function() {
});
beforeEach(function() {
- compile($rootScope, $compile)
+ compile($rootScope, $compile);
});
it('shows the default template', function() {
@@ -270,7 +321,7 @@ describe('oauth', function() {
});
beforeEach(function() {
- compile($rootScope, $compile)
+ compile($rootScope, $compile);
});
beforeEach(function() {
@@ -301,7 +352,7 @@ describe('oauth', function() {
});
beforeEach(function() {
- compile($rootScope, $compile)
+ compile($rootScope, $compile);
});
it('shows the text "Denied"', function() {
diff --git a/test/spec/services/access-token.js b/test/spec/services/access-token.js
index 67dda57..e8d9fcd 100644
--- a/test/spec/services/access-token.js
+++ b/test/spec/services/access-token.js
@@ -2,18 +2,24 @@
describe('AccessToken', function() {
- var result, $location, $sessionStorage, AccessToken, date;
+ var result, $location, Storage, IdToken, AccessToken, date;
- var fragment = 'access_token=token&token_type=bearer&expires_in=7200&state=/path';
+ var fragment = 'access_token=token&token_type=bearer&expires_in=7200&state=/path&extra=stuff';
+ var fragmentForever = 'access_token=token&token_type=bearer&state=/path&extra=stuff';
+ var fragmentImmediate = 'access_token=token&token_type=bearer&expires_in=0&state=/path&extra=stuff';
+ var fragmentWithIdToken = 'access_token=token&token_type=bearer'+
+ '&id_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ&expires_in=3600';
var denied = 'error=access_denied&error_description=error';
var expires_at = '2014-08-17T17:38:37.584Z';
+ var newExpiresIn = 9600;
var token = { access_token: 'token', token_type: 'bearer', expires_in: 7200, state: '/path', expires_at: expires_at };
beforeEach(module('oauth'));
- beforeEach(inject(function($injector) { $location = $injector.get('$location') }));
- beforeEach(inject(function($injector) { $sessionStorage = $injector.get('$sessionStorage') }));
- beforeEach(inject(function($injector) { AccessToken = $injector.get('AccessToken') }));
+ beforeEach(inject(function($injector) { $location = $injector.get('$location'); }));
+ beforeEach(inject(function($injector) { Storage = $injector.get('Storage'); }));
+ beforeEach(inject(function($injector) { IdToken = $injector.get('IdToken'); }));
+ beforeEach(inject(function($injector) { AccessToken = $injector.get('AccessToken'); }));
describe('#set', function() {
@@ -39,6 +45,38 @@ describe('AccessToken', function() {
});
});
+ describe('when sets the access token without an expiry', function() {
+
+ beforeEach(function() {
+ $location.hash(fragmentForever);
+ });
+
+ beforeEach(function() {
+ result = AccessToken.set();
+ });
+
+ it('sets #expires_at', function() {
+ expect(result.expires_at).toBe(null);
+ });
+ });
+
+ describe('when sets the access token with an expiry of 0 (immediate)', function() {
+
+ beforeEach(function() {
+ $location.hash(fragmentImmediate);
+ });
+
+ beforeEach(function() {
+ result = AccessToken.set();
+ });
+
+ it('sets #expires_at', function() {
+ var expected_date = new Date();
+ expected_date.setSeconds(expected_date.getSeconds() - 60);
+ expect(parseInt(result.expires_at/100)).toBe(parseInt(expected_date/100)); // 10 ms
+ });
+ });
+
describe('with the access token in the fragment URI', function() {
beforeEach(function() {
@@ -54,11 +92,11 @@ describe('AccessToken', function() {
});
it('removes the fragment string', function() {
- expect($location.hash()).toEqual('');
+ expect($location.hash()).toEqual('extra=stuff');
});
it('stores the token in the session', function() {
- var stored_token = $sessionStorage.token;
+ Storage.get('token');
expect(result.access_token).toEqual('token');
});
});
@@ -66,7 +104,7 @@ describe('AccessToken', function() {
describe('with the access token stored in the session', function() {
beforeEach(function() {
- $sessionStorage.token = token;
+ Storage.set('token', token);
});
beforeEach(function() {
@@ -97,10 +135,75 @@ describe('AccessToken', function() {
});
it('stores the error message in the session', function() {
- var stored_token = $sessionStorage.token;
+ Storage.get('token');
expect(result.error).toBe('access_denied');
});
});
+
+ describe('with id token and access token in the fragment', function() {
+ beforeEach(function() {
+ $location.hash(fragmentWithIdToken);
+ });
+
+ describe('when all validation passes', function() {
+ beforeEach(function() {
+ //specific validation mechanism are tested in id-token spec
+ spyOn(IdToken, 'validateIdToken').and.returnValue(true);
+ spyOn(IdToken, 'validateAccessToken').and.returnValue(true);
+ result = AccessToken.set();
+ });
+
+ it('sets the access token', function() {
+ expect(result.access_token).toEqual('token');
+ });
+
+ it('sets the id token', function() {
+ expect(result.id_token).toEqual('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ');
+ });
+
+ it('populates id token claims', function() {
+ expect(result.id_token_claims).toBeDefined();
+ expect(result.id_token_claims.name).toEqual('John Doe');
+ });
+ });
+
+ describe('when id token validation fails', function(){
+ beforeEach(function() {
+ //specific validation mechanism are tested in id-token spec
+ spyOn(IdToken, 'validateIdToken').and.returnValue(false);
+ spyOn(IdToken, 'validateAccessToken').and.returnValue(true);
+ result = AccessToken.set();
+ });
+
+ it('erases access token and id token', function() {
+ expect(result.access_token).toEqual(null);
+ expect(result.id_token).toEqual(null);
+ });
+
+ it('sets the error', function() {
+ expect(result.error).toEqual('Failed to validate token:');
+ })
+ });
+
+ describe('when access token validation fails', function(){
+ beforeEach(function() {
+ //specific validation mechanism are tested in id-token spec
+ spyOn(IdToken, 'validateIdToken').and.returnValue(true);
+ spyOn(IdToken, 'validateAccessToken').and.returnValue(false);
+ result = AccessToken.set();
+ });
+
+ it('erases access token and id token', function() {
+ expect(result.access_token).toEqual(null);
+ expect(result.id_token).toEqual(null);
+ });
+
+ it('sets the error', function() {
+ expect(result.error).toEqual('Failed to validate token:');
+ })
+ });
+
+ })
});
@@ -143,7 +246,7 @@ describe('AccessToken', function() {
});
it('reset the cache', function() {
- expect($sessionStorage.token).toBeUndefined;
+ expect(Storage.get('token')).toBeUndefined();
});
});
@@ -193,21 +296,39 @@ describe('AccessToken', function() {
expect(result).toBe(true);
});
});
+ });
- describe('with the access token stored in the session', function() {
+ describe('#updateExpiry', function () {
+ beforeEach(function () {
+ $location.hash('');
+ Storage.set('token', token);
+ AccessToken.set();
+ });
- beforeEach(function() {
- $sessionStorage.token = token;
- });
+
+ it('updates the expiry to a new time', function () {
+ AccessToken.updateExpiry(newExpiresIn);
+ expect(AccessToken.expired()).toBeFalsy();
+ var newExpiresAt = new Date();
+ newExpiresAt.setSeconds(newExpiresAt.getSeconds() + newExpiresIn - 60);
+ expect(AccessToken.get().expires_at).toEqual(newExpiresAt);
+ });
+ });
+
+ describe('#sessionExpired', function() {
+ describe('with the access token stored in the session', function() {
beforeEach(function() {
+ //It is an invalid test to have oAuth hash AND be getting token from session
+ //if hash is in URL it should always be used, cuz its coming from oAuth2 provider re-direct
+ $location.hash('');
+ Storage.set('token', token);
result = AccessToken.set().expires_at;
});
it('rehydrates the expires_at value', function() {
- expect(result).toEqual(new Date(expires_at));
+ expect(result).toEqual(expires_at);
});
});
-
});
});
diff --git a/test/spec/services/endpoint.js b/test/spec/services/endpoint.js
index 245237b..f763b30 100644
--- a/test/spec/services/endpoint.js
+++ b/test/spec/services/endpoint.js
@@ -2,17 +2,16 @@
describe('Endpoint', function() {
- var result, $location, $sessionStorage, Endpoint;
+ var result, $location, Storage, Endpoint;
- var fragment = 'access_token=token&token_type=bearer&expires_in=7200&state=/path';
- var params = { site: 'http://example.com/', clientId: 'client-id', redirectUri: 'http://example.com/redirect', scope: 'scope', authorizePath: '/oauth/authorize' };
+ var params = { site: 'http://example.com/', clientId: 'client-id', redirectUri: 'http://example.com/redirect', scope: 'scope', authorizePath: '/oauth/authorize', responseType: 'token' };
var uri = 'http://example.com/oauth/authorize?response_type=token&client_id=client-id&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=scope&state=';
beforeEach(module('oauth'));
- beforeEach(inject(function($injector) { $location = $injector.get('$location') }));
- beforeEach(inject(function($injector) { $sessionStorage = $injector.get('$sessionStorage') }));
- beforeEach(inject(function($injector) { Endpoint = $injector.get('Endpoint') }));
+ beforeEach(inject(function($injector) { $location = $injector.get('$location'); }));
+ beforeEach(inject(function($injector) { Storage = $injector.get('Storage'); }));
+ beforeEach(inject(function($injector) { Endpoint = $injector.get('Endpoint'); }));
describe('#set', function() {
@@ -43,15 +42,15 @@ describe('Endpoint', function() {
var paramsClone = JSON.parse(JSON.stringify(params));
beforeEach(function() {
- paramsClone.state = 'test';
+ paramsClone.state = 'test';
});
beforeEach(function() {
- result = Endpoint.set(paramsClone);
+ result = Endpoint.set(paramsClone);
});
it('uri should not be in state', function() {
- expect(result).toEqual(uri + 'test');
+ expect(result).toEqual(uri + 'test');
});
});
@@ -59,16 +58,33 @@ describe('Endpoint', function() {
var paramsClone = JSON.parse(JSON.stringify(params));
beforeEach(function() {
- paramsClone.authorizePath = '/oauth/authorize?google=doesthis';
+ paramsClone.authorizePath = '/oauth/authorize?google=doesthis';
});
beforeEach(function() {
- result = Endpoint.set(paramsClone);
+ result = Endpoint.set(paramsClone);
});
it('uri should not be in state', function() {
- var expectedUri = 'http://example.com/oauth/authorize?google=doesthis&response_type=token&client_id=client-id&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=scope&state=';
- expect(result).toEqual(expectedUri);
+ var expectedUri = 'http://example.com/oauth/authorize?google=doesthis&response_type=token&client_id=client-id&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=scope&state=';
+ expect(result).toEqual(expectedUri);
+ });
+ });
+
+ describe('authorizePath can be empty', function() {
+ var paramsClone = JSON.parse(JSON.stringify(params));
+
+ beforeEach(function() {
+ paramsClone.authorizePath = '';
+ });
+
+ beforeEach(function() {
+ result = Endpoint.set(paramsClone);
+ });
+
+ it('uri should not be in state', function() {
+ var expectedUri = 'http://example.com/?response_type=token&client_id=client-id&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=scope&state=';
+ expect(result).toEqual(expectedUri);
});
});
});
@@ -79,12 +95,102 @@ describe('Endpoint', function() {
Endpoint.set(params);
});
- beforeEach(function() {
- result = Endpoint.get();
+ describe( 'without overrides', function(){
+ beforeEach(function() {
+ result = Endpoint.get();
+ });
+
+ it('returns the oauth server endpoint', function() {
+ expect(result).toEqual(uri);
+ });
});
- it('returns the oauth server endpoint', function() {
- expect(result).toEqual(uri);
+ describe( 'with state override', function(){
+ it( 'injects the state correct', function(){
+ var override = { state: 'testState' };
+ var result = Endpoint.get( override );
+ expect( result ).toEqual( uri + encodeURIComponent( override.state ) );
+ });
+ });
+
+ describe( 'with clientId override', function(){
+ it( 'injects the override', function(){
+ var override = { clientId: 'unicorn' };
+ var result = Endpoint.get( override );
+ var expectedUri = 'http://example.com/oauth/authorize?response_type=token&client_id=unicorn&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=scope&state=';
+ expect( result ).toEqual( expectedUri );
+ });
+ });
+
+ describe( 'with scope override', function(){
+ it( 'injects the override', function(){
+ var override = { scope: 'stars' };
+ var result = Endpoint.get( override );
+ var expectedUri = 'http://example.com/oauth/authorize?response_type=token&client_id=client-id&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=stars&state=';
+ expect( result ).toEqual( expectedUri );
+ });
+ });
+
+ describe( 'with state override', function(){
+ it( 'injects the state correct', function(){
+ var override = { state: 'testState' };
+ var result = Endpoint.get( override );
+ expect( result ).toEqual( uri + encodeURIComponent( override.state ) );
+ });
+ });
+
+ describe( 'with responseType override', function(){
+ it( 'injects the correct repsonseType', function(){
+ var override = { responseType: 'id_token' };
+ var result = Endpoint.get( override );
+ var expectedUri = 'http://example.com/oauth/authorize?response_type=id_token&client_id=client-id&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=scope&state=';
+ expect( result ).toEqual( expectedUri );
+ });
+ });
+
+ describe( 'with site override', function(){
+ it( 'injects the correct site', function(){
+ var override = { site: 'https://invincible.test/' };
+ var result = Endpoint.get( override );
+ var expectedUri = 'https://invincible.test/oauth/authorize?response_type=token&client_id=client-id&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=scope&state=';
+ expect( result ).toEqual( expectedUri );
+ });
+ });
+
+ describe( 'with authorize path overrides', function(){
+ it( 'injects the correct authorize path', function(){
+ var override = { authorizePath: '/end/here' };
+ var result = Endpoint.get( override );
+ var expectedUri = 'http://example.com/end/here?response_type=token&client_id=client-id&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=scope&state=';
+ expect( result ).toEqual( expectedUri );
+ });
+ });
+
+ describe( 'given scope with spaces', function(){
+ it( 'correctly encodes the spaces', function(){
+ var override = { scope: 'read write profile openid' };
+ var result = Endpoint.get( override );
+ var expectedUri = 'http://example.com/oauth/authorize?response_type=token&client_id=client-id&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=read%20write%20profile%20openid&state=';
+ expect( result ).toEqual( expectedUri );
+ });
+ });
+
+ describe( 'on repsonse type with spaces', function(){
+ it( 'correctly encodes the spaces', function(){
+ var override = { responseType: 'id_token token' };
+ var result = Endpoint.get( override );
+ var expectedUri = 'http://example.com/oauth/authorize?response_type=id_token%20token&client_id=client-id&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=scope&state=';
+ expect( result ).toEqual( expectedUri );
+ });
+ });
+
+ describe( 'when provided with a nonce', function(){
+ it( 'adds the nonce parameter', function(){
+ var override = { nonce: '987654321' };
+ var result = Endpoint.get( override );
+ var expectedUri = 'http://example.com/oauth/authorize?response_type=token&client_id=client-id&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect&scope=scope&state=&nonce=987654321';
+ expect( result ).toEqual( expectedUri );
+ });
});
});
});
diff --git a/test/spec/services/id-token.js b/test/spec/services/id-token.js
new file mode 100644
index 0000000..11d72c3
--- /dev/null
+++ b/test/spec/services/id-token.js
@@ -0,0 +1,248 @@
+describe('IdToken', function() {
+
+ var Storage, IdToken;
+
+ var publicKeyString;
+ var validIdToken, invalidIdToken;
+ var validAccessToken;
+
+ beforeEach(module('oauth'));
+
+ beforeEach(inject(function ($injector) {
+ Storage = $injector.get('Storage');
+ }));
+ beforeEach(inject(function ($injector) {
+ IdToken = $injector.get('IdToken');
+ }));
+
+ beforeEach(function () {
+ /**
+ * http://kjur.github.io/jsjws/tool_jwt.html generated sample id_token, signed by default private key
+ * The public key is shown as below
+ */
+ publicKeyString =
+ "-----BEGIN PUBLIC KEY-----\n"
+ + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA33TqqLR3eeUmDtHS89qF\n"
+ + "3p4MP7Wfqt2Zjj3lZjLjjCGDvwr9cJNlNDiuKboODgUiT4ZdPWbOiMAfDcDzlOxA\n"
+ + "04DDnEFGAf+kDQiNSe2ZtqC7bnIc8+KSG/qOGQIVaay4Ucr6ovDkykO5Hxn7OU7s\n"
+ + "Jp9TP9H0JH8zMQA6YzijYH9LsupTerrY3U6zyihVEDXXOv08vBHk50BMFJbE9iwF\n"
+ + "wnxCsU5+UZUZYw87Uu0n4LPFS9BT8tUIvAfnRXIEWCha3KbFWmdZQZlyrFw0buUE\n"
+ + "f0YN3/Q0auBkdbDR/ES2PbgKTJdkjc/rEeM0TxvOUf7HuUNOhrtAVEN1D5uuxE1W\n"
+ + "SwIDAQAB"
+ + "-----END PUBLIC KEY-----\n";
+ });
+
+ describe('validate an id_token with both signature and claims', function() {
+ beforeEach(function () {
+ /*
+ Valid token with RS256, expires at 20251231235959Z UTC
+ https://jwt.io has a debugger that can help view the id_token's header and payload
+
+ e.g. The header of following token is { "alg": "RS256", "typ": "JWT" }
+ The body of the following token is:
+ {
+ "iss": "oidc",
+ "sub": "oauth-ng-client",
+ "nbf": 1449267385,
+ "exp": 1767225599,
+ "iat": 1449267385,
+ "jti": "id123456",
+ "typ": "https://example.com/register"
+ }
+ */
+ validIdToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' +
+ '.eyJpc3MiOiJvaWRjIiwic3ViIjoib2F1dGgtbmctY2xpZW50IiwibmJmIjoxNDQ5MjY3Mzg1LCJleHAiOjE3NjcyMjU1OTksImlhdCI6MTQ0OTI2NzM4NSwianRpIjoiaWQxMjM0NTYiLCJ0eXAiOiJodHRwczovL2V4YW1wbGUuY29tL3JlZ2lzdGVyIn0' +
+ '.MXBbWkr1Sf6KRn11IgEXyVg5g5VVUOSyLhTglgL8fI13aGf6SquVy0ZNn7ajTym5a_fJHPWLlgpvo-v98xuMBC9cLH_NN3ocrZAQkkW19G4AVY-LsOURp0t9JzVEb-pEe8Zps8O7Mumj0qSlr-4Dnyb3UMqdwZTcSgUTrbdyf6Qa7KHA0myANLDs2T8ctlSEptgVHPj8Zy9tk9UUlDZgsU4KoEpanDt7c1GzQJu7KEI3iJYlVEwDgMqu0EWn64aaP-w1OKZAyHbJWdMwun7i9edLonQ37M67Mb8ox6-cx8fxS3s3h6b3jRS5L0RACFVtB9lF4f_0yPVBwcTBhzYBOg';
+
+ IdToken.set({
+ issuer: 'oidc',
+ clientId: 'oauth-ng-client',
+ pubKey: publicKeyString
+ });
+ });
+
+ it('with success', function () {
+ expect(IdToken.validateIdToken(validIdToken)).toEqual(true);
+ });
+
+ });
+
+ describe('verify id_token signature with algorithm', function () {
+
+ describe('RS256', function () {
+
+ beforeEach(function () {
+
+ //Valid token with RS256, expires at 20251231235959Z UTC
+ validIdToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' +
+ '.eyJpc3MiOiJvaWRjLXJzLTI1NiIsInN1YiI6Im9hdXRoLW5nLWNsaWVudCIsIm5iZiI6MTQ0OTQ0OTE0NCwiZXhwIjoxNzY3MjI1NTk5LCJpYXQiOjE0NDk0NDkxNDQsImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9' +
+ '.lpxeRY_IqsvgWLj6H2ghre8dBBtsSF-bnjWHtVvhQIuerztMQOX20CCqtVFGScIZcI4gHxtEZGauF-sX3zwaLuqPtzORjaBiH0vV6C-3ZyqZrCU_n-TozKAwpSYyyHQpJ-xKdGRaOdd7_4vDtaFBWyHLXp1hbYvMftkPCvGjO25GppGQ7MjxCnd7IAPn0obXx2lZr1q0hHT7532O5dlmsPHTyrTvrSupTOVH3CZe3ZghM6R_mlagyfRh1Pf2cdRQkXJ0gEHf4GYpBbz-E3YfCyxcvQRPzfKnpLGH16M1_jM0mc3z5zVsegi62NNr79B8hExG5OtXfDMvws4LDfps2A';
+
+ IdToken.set({
+ issuer: 'oidc-rs-256',
+ clientId: 'oauth-ng-client',
+ pubKey: publicKeyString
+ });
+ });
+
+ it('validate token successfully', function () {
+ expect(IdToken.verifyIdTokenSig(validIdToken)).toEqual(true);
+ });
+
+ });
+
+ describe('RS384', function () {
+
+ beforeEach(function() {
+ //Valid token with RS384, expires at 20251231235959Z UTC
+ validIdToken = 'eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9' +
+ '.eyJpc3MiOiJvaWRjLXJzLTM4NCIsInN1YiI6Im9hdXRoLW5nLWNsaWVudCIsIm5iZiI6MTQ0OTQ0NTM3NSwiZXhwIjoxNzY3MjI1NTk5LCJpYXQiOjE0NDk0NDUzNzUsImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9' +
+ '.cDSfORFIZ28nUgvbdJ0wwe-JtZOy5J40--xYfJBPbwRJz02EscFlRD-xNqRB9eEsVVK4shW9AtF4yfp3Sa_jcjziGlQNwpRzFhnCqrADupMNNhK-z1SmuxgG_zfP7plXVAhg1IJ671w43I2PmXQw5wAKpAMwun4J-mxHP7ZV6__z_hxv4QclONHrk23_ebHJXi8W4q7B7n-amQQZ-kKQf8OblZIX9kAF58WIhyA5ZNqXGZ_hmcDKUVlBpgiurpD8u429NwrlauowHCQI_zMKlaEzJvH5qNhXLNbFgLmhrQFYo_VW48ZjHygmAkuuKt0jioR0dUeYirTGq-xEBcOpnw';
+
+ IdToken.set({
+ issuer: 'oidc-rs-384',
+ clientId: 'oauth-ng-client',
+ pubKey: publicKeyString
+ });
+
+ });
+
+ it('validate token successfully', function () {
+ expect(IdToken.verifyIdTokenSig(validIdToken)).toEqual(true);
+ });
+
+ });
+
+ describe('RS512', function () {
+
+ beforeEach(function() {
+ //Valid token with RS512, expires at 20251231235959Z UTC
+ validIdToken = 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9' +
+ '.eyJpc3MiOiJvaWRjLXJzLTUxMiIsInN1YiI6Im9hdXRoLW5nLWNsaWVudCIsIm5iZiI6MTQ0OTQ0NzQyMiwiZXhwIjoxNzY3MjI1NTk5LCJpYXQiOjE0NDk0NDc0MjIsImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9' +
+ '.BAqggV91_YwQmnKty-qHzi61WgUpLue7gFWDe72VzHzcQhYu1STLduBxtr3qtsl6XzVDL6N0AmYfpFwMQQ8dRMaLLsnx6wg7Mi9nqCkDTL1Px5biL9AM4C3S32N6iJ4nFyJgUiFJ4RWG9f-78k4PG51xvSCkA-2TbODU1KsXRnc3o9SrQKw8pWnjmxNIfDtfzkxEdBlePWuknZGeaJBlR4hBRrxH1GnNDVW3aeuLJl4y1IOIbUxsnNW8HgAm6KpoCVAbPN7YzQPfDEIQgaNSS_i7Nkuq9Rno_6ivfqxs3QCiEqHJkAh8W2J3N8iPpRrCW03oQp2sGvmRTxxvxuxZbw'
+
+ IdToken.set({
+ issuer: 'oidc-rs-512',
+ clientId: 'oauth-ng-client',
+ pubKey: publicKeyString
+ });
+
+ });
+
+ it('validate token successfully', function () {
+ expect(IdToken.verifyIdTokenSig(validIdToken)).toEqual(true);
+ });
+
+ });
+
+ });
+
+ describe('verify id_token using JWK(Json Web Key) format key', function() {
+ beforeEach(function () {
+ //The same public key, but using a JWK format
+ var jwkString = '{"kty":"RSA","n":"33TqqLR3eeUmDtHS89qF3p4MP7Wfqt2Zjj3lZjLjjCGDvwr9cJNlNDiuKboODgUiT4ZdPWbOiMAfDcDzlOxA04DDnEFGAf-kDQiNSe2ZtqC7bnIc8-KSG_qOGQIVaay4Ucr6ovDkykO5Hxn7OU7sJp9TP9H0JH8zMQA6YzijYH9LsupTerrY3U6zyihVEDXXOv08vBHk50BMFJbE9iwFwnxCsU5-UZUZYw87Uu0n4LPFS9BT8tUIvAfnRXIEWCha3KbFWmdZQZlyrFw0buUEf0YN3_Q0auBkdbDR_ES2PbgKTJdkjc_rEeM0TxvOUf7HuUNOhrtAVEN1D5uuxE1WSw","e":"AQAB"}';
+ //Valid token with RS256, expires at 20251231235959Z UTC
+ validIdToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' +
+ '.eyJpc3MiOiJvaWRjLXJzLTI1NiIsInN1YiI6Im9hdXRoLW5nLWNsaWVudCIsIm5iZiI6MTQ0OTQ0OTE0NCwiZXhwIjoxNzY3MjI1NTk5LCJpYXQiOjE0NDk0NDkxNDQsImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9' +
+ '.lpxeRY_IqsvgWLj6H2ghre8dBBtsSF-bnjWHtVvhQIuerztMQOX20CCqtVFGScIZcI4gHxtEZGauF-sX3zwaLuqPtzORjaBiH0vV6C-3ZyqZrCU_n-TozKAwpSYyyHQpJ-xKdGRaOdd7_4vDtaFBWyHLXp1hbYvMftkPCvGjO25GppGQ7MjxCnd7IAPn0obXx2lZr1q0hHT7532O5dlmsPHTyrTvrSupTOVH3CZe3ZghM6R_mlagyfRh1Pf2cdRQkXJ0gEHf4GYpBbz-E3YfCyxcvQRPzfKnpLGH16M1_jM0mc3z5zVsegi62NNr79B8hExG5OtXfDMvws4LDfps2A';
+
+ IdToken.set({
+ issuer: 'oidc-rs-256',
+ clientId: 'oauth-ng-client',
+ pubKey: jwkString
+ });
+ });
+
+ it('validate token successfully', function () {
+ expect(IdToken.verifyIdTokenSig(validIdToken)).toEqual(true);
+ });
+
+ });
+
+ describe('validate access_token with id_token header information', function () {
+
+ beforeEach(function() {
+ /*
+ Sample id_token and access_token pair (corresponds to response_type = 'id_token token'
+ Get more examples at google playground: https://developers.google.com/oauthplayground/
+ */
+ validIdToken = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhODc0MjBlY2YxNGU5MzRmOWY5MDRhMDE0NzY4MTMyMDNiMzk5NGIifQ' +
+ '.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTEwMTY5NDg0NDc0Mzg2Mjc2MzM0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXRfaGFzaCI6ImFVQWtKRy11Nng0UlRXdUlMV3ktQ0EiLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJpYXQiOjE0MzIwODI4NzgsImV4cCI6MTQzMjA4NjQ3OH0' +
+ '.xSwhf4KvEztFFhVj4YdgKFOC8aPEoLAAZcXDWIh6YBXpfjzfnwYhaQgsmCofzOl53yirpbj5h7Om5570yzlUziP5TYNIqrA3Nyaj60-ZyXY2JMIBWYYMr3SRyhXdW0Dp71tZ5IaxMFlS8fc0MhSx55ZNrCV-3qmkTLeTTY1_4Jc';
+ validAccessToken = 'ya29.eQETFbFOkAs8nWHcmYXKwEi0Zz46NfsrUU_KuQLOLTwWS40y6Fb99aVzEXC0U14m61lcPMIr1hEIBA';
+ });
+
+ it('should succeed', function() {
+ expect(IdToken.validateAccessToken(validIdToken, validAccessToken)).toEqual(true);
+ })
+ });
+
+
+ describe('detect false id_token with', function () {
+
+ describe('wrong issuer', function () {
+
+ beforeEach(function () {
+ //Id token with issuer as 'oidc-foo'
+ invalidIdToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' +
+ '.eyJpc3MiOiJvaWRjLWZvbyIsInN1YiI6Im9hdXRoLW5nLWNsaWVudCIsIm5iZiI6MTQ0OTQ0OTYyNCwiZXhwIjoxNzY3MjI1NTk5LCJpYXQiOjE0NDk0NDk2MjQsImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9' +
+ '.UCDvdOtXzXeDReumQsj-WXEgUIs_PyTdVt94n52hZT9q3Rfs4jFkx64n2tGa0SAzAwkQz8UhBn9omYg7c_A9Q8eYwbOLzSq8QUcH6adXME80c7ychmHsy4T8wXRhKExbSThs37Rgq38Z6mkodqYxxdGJw4xoiR3yPij2bXwT6Knes6nXEWYnhPosiLxOhzIIH7-LRPRFVd3aad0cm9TRkNzkEyZ4j2QPtNsKur80sJ0qrEFp-unjoyg59GMNF8yatt8d1hgNgnWIMSuzwRq4U4Da2Q7QMKadhArqNY1mDZJl3duS8No57RMPYipq2y8DVEqKzE2ie-jNs1fmB67hqQ';
+
+ IdToken.set({
+ issuer: 'oidc',
+ clientId: 'oauth-ng-client',
+ pubKey: publicKeyString
+ });
+ });
+
+ it('should throw exception', function () {
+ expect(function(){ IdToken.verifyIdTokenInfo(invalidIdToken) }).toThrowError(/Invalid issuer/);
+ });
+
+ });
+
+ describe('future issue time', function () {
+
+ beforeEach(function () {
+ //Id token issued in the future (20251231235959Z UTC)
+ invalidIdToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' +
+ '.eyJpc3MiOiJvaWRjLWZvbyIsInN1YiI6Im9hdXRoLW5nLWNsaWVudCIsIm5iZiI6MTQ0OTQ1MDk5NCwiZXhwIjoxNzY3MjI1NTk5LCJpYXQiOjE3NjcyMjU1OTksImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9' +
+ '.d7NrttJitzksr_wJzfvNQJhKTUa2Lp0YIl3B-FNUROMZ4nEGqW385ZwcIZlv1A2BlwbiozZBZ7rpiHHb3yAyfUToL8JD8hzfVurboc63Vp3qpHEMNzLIuWD4AUcYeuBIGz_gIT2sNeltjqJTPFUNm5FPRIs4O-a5b-13rosxI5UhQ7m6MLCUJ_U7w5Jxl5Dei2MUM3dF9ugI5UC17YFsAqWeAnddT2m9TPQGvTS8G42iuEOKxLIBkqE9SCRhcpRy66DWKNi8yyroLMIM9UOiyh2ODrI2sBn1TVa9b6-XkDGwDZdlbc2AWiGLFD2KeoBFYKV03aHhoWL2J9UFs08O8Q';
+
+ IdToken.set({
+ issuer: 'oidc',
+ clientId: 'oauth-ng-client',
+ pubKey: publicKeyString
+ });
+ });
+
+ it('should throw exception', function () {
+ expect(function(){ IdToken.verifyIdTokenInfo(invalidIdToken) }).toThrowError(/issued time is later than current time/);
+ });
+
+ });
+
+ describe('expired token', function () {
+
+ beforeEach(function () {
+ //Expired id token
+ invalidIdToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' +
+ '.eyJpc3MiOiJvaWRjLWZvbyIsInN1YiI6Im9hdXRoLW5nLWNsaWVudCIsIm5iZiI6MTQ0OTQ1MTIxMywiZXhwIjoxNDQ5NDUxMjEzLCJpYXQiOjE0NDk0NTEyMTMsImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9' +
+ '.PAUYJX7v7xDRAVaIUNME7VxK5j2GFDE19bMaa6I-jFa5Lw5f0WlXBOa2WXYqynPymtC0-UMTdUeZ4V_mA07ubTNKyyyqNelr-kpGvM3NzIZpHokEibQVF3JeK1pH_pqnC_MYHePZMiejCkSSPMvC1_lvPOMiMfEhqWvqh58aw7v8q9a9OQYsTlQU_q_rq4mTvDkv9gjU8qKqFInLKIU1TZn4tnslFroW70kvOndz8MHOmXCyQOLbyDW9NHgJXCCCxXwEmo00LjxDHQOSC5uMK9mkix513AqZ8Gaj2QB7-4m6rCK23TiffGgIIlLzPq2RPSBbHGv-K5S_lR_Qh8STGA';
+
+ IdToken.set({
+ issuer: 'oidc',
+ clientId: 'oauth-ng-client',
+ pubKey: publicKeyString
+ });
+ });
+
+ it('should throw exception', function () {
+ expect(function(){ IdToken.verifyIdTokenInfo(invalidIdToken) }).toThrowError(/ID Token expired/);
+ });
+
+ });
+ });
+
+
+});
diff --git a/test/spec/services/oauth-configuration.js b/test/spec/services/oauth-configuration.js
new file mode 100644
index 0000000..4ff6796
--- /dev/null
+++ b/test/spec/services/oauth-configuration.js
@@ -0,0 +1,70 @@
+'use strict';
+
+describe('AuthInterceptor', function() {
+ var theOAuthConfigurationProvider, theOAuthConfiguration, OAuthConfiguration, AccessToken, AuthInterceptor, $location, result;
+ var protectedConfig, unprotectedConfig;
+
+ beforeEach(module('oauth'));
+
+ var expires_at = '2014-08-17T17:38:37.584Z';
+ var fragment = 'access_token=token&token_type=bearer&expires_in=7200&state=/path&extra=stuff'
+
+
+ beforeEach(function() {
+ var fakeModule = angular.module('test.app.config', function(){});
+ fakeModule.config(function(OAuthConfigurationProvider, $httpProvider) {
+ theOAuthConfigurationProvider = OAuthConfigurationProvider;
+ theOAuthConfigurationProvider.init({protectedResources:['http://api.protected/']}, $httpProvider);
+ theOAuthConfiguration = theOAuthConfigurationProvider.$get();
+ })
+ module('oauth', 'test.app.config');
+
+ inject(function() {});
+ })
+
+ beforeEach(inject(function() { OAuthConfiguration = theOAuthConfiguration }));
+ beforeEach(inject(function($injector) { $location = $injector.get('$location'); }));
+ beforeEach(inject(function($injector) { AccessToken = $injector.get('AccessToken'); AccessToken.destroy(); } ));
+ beforeEach(inject(function($injector) { AuthInterceptor = $injector.get('AuthInterceptor'); }));
+
+ beforeEach(function() {
+ protectedConfig = { url: 'http://api.protected/', headers: { 'Accept': 'application/json'} };
+ unprotectedConfig = { url: 'http://api.unprotected/', headers: { 'Accept': 'application/json' } };
+ });
+
+ describe('when the resource is protected', function() {
+ beforeEach(function() {
+ $location.hash(fragment);
+ AccessToken.set();
+ result = AuthInterceptor.request(protectedConfig);
+ });
+
+ it('should have the token in the header', function() {
+ expect(result.headers.Authorization).toEqual('Bearer token');
+ });
+ });
+
+ describe('when the resource is unprotected', function() {
+ beforeEach(function() {
+ $location.hash(fragment);
+ AccessToken.set();
+ result = AuthInterceptor.request(unprotectedConfig);
+ });
+
+ it('should not have the token in the header', function() {
+ expect(result.headers.Authorization).toBeUndefined();
+ });
+ });
+
+ describe('when the user is not logged in', function() {
+ beforeEach(function() {
+ AccessToken.destroy();
+ result = AuthInterceptor.request(protectedConfig);
+ });
+
+ it('should not have anything in the authorization header', function() {
+ expect(result.headers.Authorization).toBeUndefined();
+ });
+ });
+
+})
\ No newline at end of file
diff --git a/test/spec/services/oidc-config.js b/test/spec/services/oidc-config.js
new file mode 100644
index 0000000..22c76cc
--- /dev/null
+++ b/test/spec/services/oidc-config.js
@@ -0,0 +1,87 @@
+describe('IdToken', function() {
+
+ var IdToken, OidcConfig;
+
+ var publicKeyModulus, publicKeyExponent;
+ var validIdToken;
+ var $httpBackend;
+
+ beforeEach(module('oauth'));
+
+ beforeEach(inject(function ($injector) {
+ IdToken = $injector.get('IdToken');
+ }));
+
+ beforeEach(function () {
+ /**
+ * http://kjur.github.io/jsjws/tool_jwt.html generated sample id_token, signed by default private key
+ * The public key modulus and exponent are as below. This is the same public key as in id-token.js test.
+ */
+ publicKeyModulus = '33TqqLR3eeUmDtHS89qF3p4MP7Wfqt2Zjj3lZjLjjCGDvwr9cJNlN' +
+ 'DiuKboODgUiT4ZdPWbOiMAfDcDzlOxA04DDnEFGAf-kDQiNSe2Ztq' +
+ 'C7bnIc8-KSG_qOGQIVaay4Ucr6ovDkykO5Hxn7OU7sJp9TP9H0JH8' +
+ 'zMQA6YzijYH9LsupTerrY3U6zyihVEDXXOv08vBHk50BMFJbE9iwF' +
+ 'wnxCsU5-UZUZYw87Uu0n4LPFS9BT8tUIvAfnRXIEWCha3KbFWmdZQ' +
+ 'ZlyrFw0buUEf0YN3_Q0auBkdbDR_ES2PbgKTJdkjc_rEeM0TxvOUf' +
+ '7HuUNOhrtAVEN1D5uuxE1WSw';
+ publicKeyExponent = 'AQAB';
+ });
+
+ beforeEach(inject(function ($injector) {
+ OidcConfig = $injector.get('OidcConfig');
+ $httpBackend = $injector.get('$httpBackend');
+ $httpBackend.when('GET', 'oidc/.well-known/openid-configuration')
+ .respond({jwks_uri: "oidc/jwks_uri"});
+ $httpBackend.when('GET', 'oidc/jwks_uri')
+ .respond({
+ keys: [{
+ kty: 'RSA',
+ n: publicKeyModulus,
+ e: publicKeyExponent,
+ kid: 'rsa1'
+ }
+ ]
+ });
+ }));
+
+
+ describe('validate an id_token when pubkey is loaded from .well-known configuration', function() {
+ beforeEach(function () {
+ /*
+ Valid token with RS256, expires at 20251231235959Z UTC
+ https://jwt.io has a debugger that can help view the id_token's header and payload
+
+ e.g. The header of following token is { "alg": "RS256", "typ": "JWT", "kid": "rsa1" }
+ The body of the following token is:
+ {
+ "iss": "oidc",
+ "sub": "oauth-ng-client",
+ "nbf": 1449267385,
+ "exp": 1767225599,
+ "iat": 1449267385,
+ "jti": "id123456",
+ "typ": "https://example.com/register"
+ }
+ */
+ validIdToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InJzYTEifQ' +
+ '.eyJpc3MiOiJvaWRjIiwic3ViIjoib2F1dGgtbmctY2xpZW50IiwibmJmIjoxNDQ5MjY3Mzg1LCJleHAiOjE3NjcyMjU1OTksImlhdCI6MTQ0OTI2NzM4NSwianRpIjoiaWQxMjM0NTYiLCJ0eXAiOiJodHRwczovL2V4YW1wbGUuY29tL3JlZ2lzdGVyIn0' +
+ '.J39k8LK_lu7xYvW_eU-MAI3jtgQEdkpJOlx4ZX_WZ3TUyY-9GG0xLUusteDcy3UGIVxangwonaZ7311WKKz9OwjU1ePivMLXayiP2bL6srIUu8PvOOIcf8oPt8HGv-TLb1zPmYPx3XniekKUEnFAxMGedobcX0wg9tZnnkM11T4qQPcTjDhKB9bNlih9yRHR-6OkKZN4Q_by7EJAPJti22L0dTCW81A_9J5OMXoe0k6fScGfc0Wspsc7CpBN9ZAmTUdHGe8IP5L4leM0pOud6M0gzcIhixR1OMm6qj7ZyvJxgZ48h7Fln3CHyz3LGoHBTQWWDf3ufTzl3sGvippc1w';
+
+ var scope = {
+ issuer: 'oidc',
+ clientId: 'oauth-ng-client',
+ wellKnown: 'sync'
+ };
+
+ OidcConfig.load(scope).then(function(){
+ IdToken.set(scope);
+ })
+ $httpBackend.flush();
+ });
+
+ it('with success', function () {
+ expect(IdToken.validateIdToken(validIdToken)).toEqual(true);
+ });
+
+ });
+});
diff --git a/test/spec/services/profile.js b/test/spec/services/profile.js
index 3eedbe9..dea4bed 100644
--- a/test/spec/services/profile.js
+++ b/test/spec/services/profile.js
@@ -1,105 +1,120 @@
-//'use strict';
+'use strict';
-//describe('Profile', function() {
-
- //var $rootScope, $location, $httpBackend, $http, AccessToken, Profile;
- //var result, date, callback;
-
- //var fragment = 'access_token=token&token_type=bearer&expires_in=7200&state=/path';
- //var headers = { 'Accept': 'application/json, text/plain, */*', 'Authorization': 'Bearer token' }
- //var params = { site: 'http://example.com/', client: 'client-id', redirect: 'http://example.com/redirect', scope: 'scope', profileUri: 'http://example.com/me' };
- //var resource = { id: '1', name: 'Alice' };
-
- //beforeEach(module('oauth'));
-
- //beforeEach(inject(function($injector) { $rootScope = $injector.get('$rootScope') }));
- //beforeEach(inject(function($injector) { $location = $injector.get('$location') }));
- //beforeEach(inject(function($injector) { $httpBackend = $injector.get('$httpBackend') }));
- //beforeEach(inject(function($injector) { $http = $injector.get('$http') }));
- //beforeEach(inject(function($injector) { AccessToken = $injector.get('AccessToken') }));
- //beforeEach(inject(function($injector) { Profile = $injector.get('Profile') }));
-
- //beforeEach(function() { callback = jasmine.createSpy('callback') });
-
-
- //describe('.get', function() {
-
- //describe('when authenticated', function() {
-
- //beforeEach(function() {
- //$location.hash(fragment);
- //AccessToken.set(params);
- //});
-
- //beforeEach(function() {
- //$httpBackend.whenGET('http://example.com/me', headers).respond(resource);
- //});
-
- //describe('when gets the profile', function() {
-
- //it('makes the request', function() {
- //$httpBackend.expect('GET', 'http://example.com/me');
- //Profile.find(params.profileUri);
- //$rootScope.$apply();
- //$httpBackend.flush();
- //});
-
- //it('gets the resource', inject(function(Profile) {
- //Profile.find(params.profileUri).success(function(response) { result = response });
- //$rootScope.$apply();
- //$httpBackend.flush();
- //expect(result.name).toEqual('Alice');
- //}));
-
- //it('caches the profile', function() {
- //Profile.find(params.profileUri);
- //$rootScope.$apply();
- //$httpBackend.flush();
- //expect(Profile.get().name).toEqual('Alice');
- //});
-
- //describe('when expired', function() {
-
- //beforeEach(function() {
- //$rootScope.$on('oauth:expired', callback);
- //});
-
- //beforeEach(function() {
- //date = new Date();
- //date.setTime(date.getTime() + 86400000);
- //});
-
- //beforeEach(function() {
- //Timecop.install();
- //Timecop.travel(date); // go one day in the future
- //});
-
- //afterEach(function() {
- //Timecop.uninstall();
- //});
-
- //it('fires the oauth:expired event', inject(function(Profile) {
- //Profile.find(params.profileUri);
- //$rootScope.$apply();
- //$httpBackend.flush();
- //var event = jasmine.any(Object);
- //var token = jasmine.any(Object);
- //expect(callback).toHaveBeenCalledWith(event, token);
- //}));
- //});
- //});
-
-
- //describe('when sets the profile', function() {
-
- //beforeEach(function() {
- //Profile.set(resource);
- //});
-
- //it('caches the profile', function() {
- //expect(Profile.get().name).toEqual('Alice');
- //});
- //});
- //});
- //});
-//});
+describe('Profile', function() {
+
+ var $rootScope, $location, $httpBackend, $http, AccessToken, Profile;
+ var result, date, callback;
+
+ var fragment = 'access_token=token&token_type=bearer&expires_in=7200&state=/path';
+ var headers = { 'Accept': 'application/json, text/plain, */*', 'Authorization': 'Bearer token' };
+ var params = { site: 'http://example.com/', client: 'client-id', redirect: 'http://example.com/redirect', scope: 'scope', profileUri: 'http://example.com/me' };
+ var resource = { id: '1', name: 'Alice' };
+
+ beforeEach(module('oauth'));
+
+ beforeEach(inject(function($injector) { $rootScope = $injector.get('$rootScope'); }));
+ beforeEach(inject(function($injector) { $location = $injector.get('$location'); }));
+ beforeEach(inject(function($injector) { $httpBackend = $injector.get('$httpBackend'); }));
+ beforeEach(inject(function($injector) { $http = $injector.get('$http'); }));
+ beforeEach(inject(function($injector) { AccessToken = $injector.get('AccessToken'); }));
+ beforeEach(inject(function($injector) { Profile = $injector.get('Profile'); }));
+
+ beforeEach(function() { callback = jasmine.createSpy('callback'); });
+
+
+ describe('.get', function() {
+
+ describe('when authenticated', function() {
+
+ beforeEach(function() {
+ $location.hash(fragment);
+ AccessToken.set(params);
+ });
+
+ beforeEach(function() {
+ $httpBackend.whenGET('http://example.com/me', headers).respond(resource);
+ });
+
+ describe('when gets the profile', function() {
+
+ it('makes the request', function() {
+ $httpBackend.expect('GET', 'http://example.com/me');
+ Profile.find(params.profileUri);
+ $rootScope.$apply();
+ $httpBackend.flush();
+ });
+
+ it('gets the resource', inject(function(Profile) {
+ Profile.find(params.profileUri).then(function(response) { result = response.data; });
+ $rootScope.$apply();
+ $httpBackend.flush();
+ expect(result.name).toEqual('Alice');
+ }));
+
+ it('caches the profile', function() {
+ Profile.find(params.profileUri);
+ $rootScope.$apply();
+ $httpBackend.flush();
+ expect(Profile.get().name).toEqual('Alice');
+ });
+
+ it('fires oauth:profile event', function(done) {
+
+ $rootScope.$on('oauth:profile', function (event, profile) {
+ expect(typeof profile).toBe('object');
+ done();
+ });
+
+ Profile.find(params.profileUri);
+ $rootScope.$apply();
+ $httpBackend.flush();
+
+ });
+
+
+ describe('when expired', function() {
+
+ beforeEach(function() {
+ $rootScope.$on('oauth:expired', callback);
+ });
+
+ beforeEach(function() {
+ date = new Date();
+ date.setTime(date.getTime() + 86400000);
+ });
+
+ beforeEach(function() {
+ Timecop.install();
+ Timecop.travel(date); // go one day in the future
+ });
+
+ afterEach(function() {
+ Timecop.uninstall();
+ });
+
+ it('fires the oauth:expired event', inject(function(Profile) {
+ Profile.find(params.profileUri);
+ $rootScope.$apply();
+ $httpBackend.flush();
+ var event = jasmine.any(Object);
+ var token = jasmine.any(Object);
+ expect(callback).toHaveBeenCalledWith(event, token);
+ }));
+ });
+ });
+
+
+ describe('when sets the profile', function() {
+
+ beforeEach(function() {
+ Profile.set(resource);
+ });
+
+ it('caches the profile', function() {
+ expect(Profile.get().name).toEqual('Alice');
+ });
+
+ });
+ });
+ });
+});
diff --git a/test/spec/services/storage.js b/test/spec/services/storage.js
new file mode 100644
index 0000000..a88fe53
--- /dev/null
+++ b/test/spec/services/storage.js
@@ -0,0 +1,103 @@
+'use strict';
+
+describe('Storage', function() {
+
+ var $sessionStorage, $localStorage, Storage;
+ var token = 'MOCK TOKEN';
+
+ beforeEach(module('oauth'));
+
+ beforeEach(inject(function($injector) { $sessionStorage = $injector.get('$sessionStorage'); }));
+ beforeEach(inject(function($injector) { $localStorage = $injector.get('$localStorage'); }));
+ beforeEach(inject(function($injector) { Storage = $injector.get('Storage'); }));
+
+ it('should use sessionStorage by default', function () {
+ expect(Storage.storage).toEqual($sessionStorage);
+ });
+
+ describe('#use', function () {
+
+ beforeEach(function () {
+ $sessionStorage.$reset();
+ $localStorage.$reset();
+ Storage.storage = null;
+ });
+
+ it('should use sessionStorage when specified', function () {
+ Storage.use('sessionStorage');
+ expect(Storage.storage).toEqual($sessionStorage);
+ });
+
+ it('should use localStorage when specified', function () {
+ Storage.use('localStorage');
+ expect(Storage.storage).toEqual($localStorage);
+ });
+
+ });
+
+ describe('#set', function() {
+
+ beforeEach(function () {
+ $sessionStorage.$reset();
+ $localStorage.$reset();
+ });
+
+ it('should set something in sessionStorage', function () {
+ Storage.storage = $sessionStorage;
+ Storage.set('token', token);
+ expect($sessionStorage.token).toEqual(token);
+ });
+
+ it('should set something in sessionStorage', function () {
+ Storage.storage = $localStorage;
+ Storage.set('token', token);
+ expect($localStorage.token).toEqual(token);
+ });
+
+ });
+
+ describe('#get', function() {
+
+ beforeEach(function () {
+ $sessionStorage.$reset();
+ $localStorage.$reset();
+ });
+
+ it('should set something in sessionStorage', function () {
+ $sessionStorage.token = token;
+ Storage.storage = $sessionStorage;
+ expect(Storage.get('token')).toEqual(token);
+ });
+
+ it('should set something in sessionStorage', function () {
+ $localStorage.token = token;
+ Storage.storage = $localStorage;
+ expect(Storage.get('token')).toEqual(token);
+ });
+
+ });
+
+ describe('#delete', function() {
+
+ beforeEach(function () {
+ $sessionStorage.$reset();
+ $localStorage.$reset();
+ });
+
+ it('should delete the token from the sessionStorage', function () {
+ $sessionStorage.token = token;
+ Storage.storage = $sessionStorage;
+ expect(Storage.delete('token')).toEqual(token);
+ expect($sessionStorage.token).not.toBeDefined();
+ });
+
+ it('should delete the token from the sessionStorage', function () {
+ $localStorage.token = token;
+ Storage.storage = $localStorage;
+ expect(Storage.delete('token')).toEqual(token);
+ expect($localStorage.token).not.toBeDefined();
+ });
+
+ });
+
+});