├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── oidc-angular.js └── sample ├── angular-base64.js ├── gulpfile.js ├── index.html └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | sample/node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michael Schnyder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oidc-angular 2 | This is an angularjs client library to support modern web-applications when using the OpenId compatible "Hyprid Flow" aka "Browser-Flow". See http://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth for details. 3 | 4 | ![Hybrid Flow explained](http://www.websequencediagrams.com/cgi-bin/cdraw?lz=dGl0bGUgQXV0aGVudGljYXRpb24gU2VxdWVuY2UKCkJyb3dzZXItPlBvcnRhbC1DbGllbnQ6IE5hdmlnYXRlIHRvIAAVBgoAFQ0AKAlBcGk6IEFjY2VzcyBSZXNzb3VyY2UKbm90ZSByaWdodCBvZgA6BwBTCVVzdWFsbHkgdGhlIGMAawUgaXMgYWJsAGkFY2hlY2sgXG50b2tlbiBwcmlvciBhAFsFaW5nAC8FQVBJAIEFCEFwaQCBKhFBdXRoIFJlcXVpcmVkCgCBJxAAgWoHOiBSZWRpcmVjdCB0byBJZFAAggAKSWRQOiBMb2dpbiB3aXRoIFVzZXJuYW1lIC8gUGFzc3dvcmQKSWRQAEALU3VjZWVkZWQsIHIASgsAgkUNAIIDDwB9CVRoZSAAgWQGaXMgdGFuc3BvcnRlZCBhIGFuIFVybC1cbkNvbXBvbmVudCBsaWtlICZpZF8AghcFPS4uLgCDJxkAgwsHAINQBgCBQgZUb2tlbgCDICYAgyobAINrBUdyYW50AINvBw&s=roundgreen) 5 | 6 | See origin on http://blog.emtwo.ch/jwt-token-based-auth-with-angularjs/ for motivation and technical details. 7 | 8 | ## Getting started 9 | To install oidc-angular use bower 10 | ``` 11 | bower install oidc-angular -save 12 | ``` 13 | 14 | Inject the `$auth`-provider to setup the library while configuring your angular-application 15 | 16 | ```javascript 17 | 18 | var app = angular.module('myApp', ['oidc-angular'], function($auth) { 19 | $auth.configure( 20 | { 21 | clientId: 'abcd...', 22 | ... 23 | } 24 | ); 25 | } 26 | ); 27 | ``` 28 | 29 | ## Configuration Options 30 | 31 | For a complete and always up-to-date list of configuration options, see https://github.com/michaelschnyder/oidc-angular/blob/master/oidc-angular.js#L220. 32 | 33 | | Option | Type | Description | Default Value | 34 | |:--------------------------|:---------|:-------------------------|:----------------------| 35 | | `basePath` | `string` | Path to the OIDC-Compatible Identity Provider. Will be used as the baseUrl for the `authorizationEndpoint` and `endSessionEndpoint` | *none* 36 | | `clientId` | `string` | The identifier of the client application. See [Spec](http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) | *none* 37 | | `apiUrl` | `string` | The url to your backend which should be protected by adding the jwt-token for outgoing requests. | `/api/` 38 | | `responseType` | `string` | Type of the required token. Should be `id_token`. | `id_token` 39 | | `scope` | `string` | Scopes (and contained claims) that should be returned by the IdP. Needs to be at least `openid profile`. Separate by space. | `openid profile` 40 | | `redirectUri` | `string` | The uri where the **Library** has registered its callback route for login is by default `#/auth/callback/`. The callback route gets evaluated by the **Library** and typically doesn't need an adjustment | `[Proto]://[HostName]/[Path(s)]/#/auth/callback/` 41 | | `logoutUri` | `string` | The uri where the **Library** has registered its callback route for logout is by default `#/auth/clear`. The callback route gets evaluated by the **Library** and typically doesn't need an adjustment | `[Proto]://[HostName]/[Path(s)]/#/auth/clear` 42 | | `authorizationEndpoint` | `string` | Place where the user logs in to the IdP. Combined with `basePath` | `[basePath]:connect/authorize` 43 | | `endSessionEndpoint` | `string` | Place where the ends his session in the IdP. Combined with `basePath` | `[basePath]:connect/endsession` 44 | | `advanceRefresh` | `int` | Defines the advance seconds when trying to silenty reaquire a token. Checks are not made constantly, only after on sucessfull responses | `300` 45 | | `enableRequestChecks` | `boolean`| Specifies if the token should be validated before using it in outgoing requests. Use with caution, because this checks depend on the currect UTC time of both the browser and the server | `false` 46 | | `stickToLastKnownIdp` | `boolean` | Defines if the user should pass the last used IdP Name with the authorization request so that a front facing authorization decision server could automatically redirect to the propper IdP. This option might only be usful when using an primary IdP with additional "child"-IdPs. See 'acr_values' on [IdentityServer Docs]( https://identityserver.github.io/Documentation/docsv2/endpoints/authorization.html) | `false` | 47 | 48 | ### Configuring the IdP 49 | When configuring the IdP, make sure the options `clientId`, `redirectUri` and `logoutUri` are exact the same as in the oidc-angular configuration. Otherwise, the IdP typically refuses to redirect back to your application as part of its security restrictions. 50 | 51 | ## Events 52 | oidc-angular comes with a various list of events which gives you the most possible flexibility to handle the authentication process 53 | 54 | Events are broadcasted to the `$rootScope`. 55 | 56 | | Name | Description | Parameters | 57 | |:--------------------------------|:----------------------------------|:----------------------| 58 | |`oidcauth:unauthorized` | The server returned an 401 response and oidc-angular was unable to find out the exect reason. See `tokenExpired` or `tokenMissing`| The `response` istelf | 59 | |`oidcauth:tokenExpired` | The server returned an 401 response and the lib found out that the token might be expired. | `request` or `response`, see `enableRequestChecks` 60 | |`oidcauth:tokenMissing` | The server returned an 401 response while the client had no token sent. | `request` or `response`, see `enableRequestChecks` 61 | |`oidcauth:tokenExpires` | Raised when the token will expire soon, based on the value of `advanceRefresh` | *none* 62 | |`oidcauth:loggedIn` | Raised when the library sucessfully parsed the token after the IdP-Redirect | *none* 63 | |`oidcauth:loggedOut` | Raised when the IdP redirected the user back to the app after logout | *none* 64 | |`oidcauth:silentRefreshStarted` | The Refresh-process of the token has started in the background (`iframe`) | *none* 65 | |`oidcauth:silentRefreshSucceded` | A new and newer token was aquired sucessfully | *none* 66 | |`oidcauth:silentRefreshFailed` | Unable to aquire a new token via background-process | *none* 67 | |`oidcauth:silentRefreshTimeout` | The background-refresh process timed out | *none* 68 | 69 | ## Methods 70 | 71 | ### SignIn 72 | 73 | Redirects the user to the configured IpP. The URL to the login screen is constructed based on the configuration made. 74 | 75 | **Samples** 76 | ```javascript 77 | $auth.signIn(); 78 | ``` 79 | 80 | Or with a redirection after login: 81 | 82 | ```javascript 83 | $auth.signIn('#/page2'); 84 | ``` 85 | 86 | ### SignOut 87 | Logout the user imediately and quit the session on the IdP by calling the `endSessionEndpoint`. Claims in local storage get cleared after callback. 88 | 89 | **Sample** 90 | 91 | ```javascript 92 | $auth.signOut(); 93 | ``` 94 | 95 | ### IsAuthenticated 96 | Returns `true` if the there is a valid token available, `false` if no token or an expired / not yet valid token is available. 97 | 98 | **Sample** 99 | 100 | ```javascript 101 | $auth.isAuthenthicated(); 102 | ``` 103 | 104 | ### IsAuthenticatedIn(milliseconds) 105 | Returns `true` if the current token is still valid after the given amount of milliseconds 106 | 107 | **Sample** 108 | 109 | ```javascript 110 | $auth.isAuthenthicatedIn(3600000); // 1hour 111 | ``` 112 | 113 | # Sample 114 | There is a sample in the `samples`-Folder. 115 | 116 | # Compatibility 117 | This library has been tested and intensively used with the ThinkTecture IdentityServer3 with various versions. Please see [Thinktecture IdentityServer3](https://github.com/IdentityServer/IdentityServer3) for further details. 118 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oidc-angular", 3 | "version": "0.0.9", 4 | "authors": [ 5 | "Michael Schnyder " 6 | ], 7 | "description": "OpenId Connect for AngularJS", 8 | "main": "oidc-angular.js", 9 | "license": "MIT", 10 | "dependencies": { 11 | "angular": "~1.3.15", 12 | "angular-route": "~1.3.15", 13 | "ngstorage": "~0.3.4", 14 | "angular-base64": "~2.0.5" 15 | }, 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "sample", 21 | "test", 22 | "tests" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /oidc-angular.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | 5 | var eventPrefix = 'oidcauth:'; 6 | 7 | var unauthorizedEvent = eventPrefix + 'unauthorized'; 8 | var tokenExpiredEvent = eventPrefix + 'tokenExpired'; 9 | var tokenMissingEvent = eventPrefix + 'tokenMissing'; 10 | var tokenExpiresSoonEvent = eventPrefix + 'tokenExpires'; 11 | 12 | var loggedInEvent = eventPrefix + 'loggedIn'; 13 | var loggedOutEvent = eventPrefix + 'loggedOut'; 14 | 15 | var silentRefreshStartedEvent = eventPrefix + 'silentRefreshStarted'; 16 | var silentRefreshSuceededEvent = eventPrefix + 'silentRefreshSucceded'; 17 | var silentRefreshFailedEvent = eventPrefix + 'silentRefreshFailed'; 18 | var silentRefreshTimeoutEvent = eventPrefix + 'silentRefreshTimeout'; 19 | 20 | // Module registrarion 21 | var oidcmodule = angular.module('oidc-angular', ['base64', 'ngStorage', 'ngRoute']); 22 | 23 | oidcmodule.config(['$httpProvider', '$routeProvider', function($httpProvider, $routeProvider) { 24 | $httpProvider.interceptors.push('oidcHttpInterceptor'); 25 | 26 | // Register callback route 27 | $routeProvider. 28 | when('/auth/callback/:data', { 29 | template: '', 30 | controller: ['$auth', '$routeParams', function ($auth, $routeParams) { 31 | console.debug('oidc-angular: handling login-callback'); 32 | $auth.handleSignInCallback($routeParams.data); 33 | }] 34 | }). 35 | when('/auth/clear', { 36 | template: '', 37 | controller: ['$auth', function ($auth) { 38 | console.debug('oidc-angular: handling logout-callback'); 39 | $auth.handleSignOutCallback(); 40 | }] 41 | }); 42 | 43 | console.debug('oidc-angular: callback routes registered.') 44 | }]); 45 | 46 | oidcmodule.factory('oidcHttpInterceptor', ['$rootScope', '$q', '$auth', 'tokenService', function($rootScope, $q, $auth, tokenService) { 47 | return { 48 | 49 | 'request': function(request) { 50 | 51 | if (request.url.startsWith($auth.config.apiUrl)) { 52 | 53 | var appendBearer = false; 54 | 55 | if($auth.config.enableRequestChecks) { 56 | // Only append token when it's valid. 57 | if (tokenService.hasToken()) { 58 | if (tokenService.hasValidToken()) 59 | { 60 | appendBearer = true; 61 | } 62 | else { 63 | $rootScope.$broadcast(tokenExpiredEvent, { request: request }); 64 | } 65 | } 66 | else { 67 | $rootScope.$broadcast(tokenMissingEvent, { request: request }); 68 | } 69 | } 70 | else { 71 | appendBearer = tokenService.hasToken(); 72 | } 73 | 74 | if (appendBearer) { 75 | var token = tokenService.getIdToken(); 76 | request.headers['Authorization'] = 'Bearer ' + token; 77 | } 78 | } 79 | 80 | // do something on success 81 | return request; 82 | }, 83 | 84 | 'response': function(response) { 85 | // Proactively check if the token will expire soon 86 | $auth.validateExpirity(); 87 | 88 | return response; 89 | }, 90 | 91 | 'responseError': function(response) { 92 | 93 | if (response.status == 401) { 94 | if (!tokenService.hasToken()) { 95 | // There was probably no token attached, because there is none 96 | $rootScope.$broadcast(tokenMissingEvent, { response: response }); 97 | } 98 | else if (!tokenService.hasValidToken()) { 99 | // Seems the token is not valid anymore 100 | $rootScope.$broadcast(tokenExpiredEvent, { response: response }); 101 | } 102 | else { 103 | // any other 104 | $rootScope.$broadcast(unauthorizedEvent, { response: response }); 105 | } 106 | } 107 | 108 | return $q.reject(response); 109 | } 110 | }; 111 | }]); 112 | 113 | oidcmodule.service('tokenService', ['$base64', '$localStorage', function ($base64, $localStorage) { 114 | 115 | var service = this; 116 | 117 | var sanitize = function (base64data) { 118 | 119 | // Pad lenght to comply with the standard 120 | while (base64data.length % 4 !== 0) { 121 | base64data += "="; 122 | } 123 | 124 | // convert to base64 from base64url 125 | base64data = base64data.replace('_', '/'); 126 | base64data = base64data.replace('-', '+'); 127 | 128 | return base64data; 129 | }; 130 | 131 | service.getPayloadFromRawToken = function(raw) 132 | { 133 | var tokenParts = raw.split("."); 134 | return tokenParts[1]; 135 | }; 136 | 137 | service.deserializeClaims = function(raw) { 138 | var claimsBase64 = sanitize(raw); 139 | var claimsJson = $base64.decode(claimsBase64); 140 | 141 | var claims = JSON.parse(claimsJson); 142 | 143 | return claims; 144 | }; 145 | 146 | service.convertToClaims = function(id_token) { 147 | var payload = service.getPayloadFromRawToken(id_token); 148 | var claims = service.deserializeClaims(payload); 149 | 150 | return claims; 151 | }; 152 | 153 | service.saveToken = function (id_token) { 154 | $localStorage['idToken'] = id_token; 155 | 156 | var idClaims = service.convertToClaims(id_token); 157 | $localStorage['cached-claims'] = idClaims; 158 | }; 159 | 160 | service.hasToken = function() { 161 | 162 | var claims = service.allClaims(); 163 | 164 | if (!(claims && claims.hasOwnProperty("iat") && claims.hasOwnProperty('exp'))) { 165 | return false; 166 | } 167 | 168 | return true; 169 | }; 170 | 171 | service.hasValidToken = function() { 172 | if (!this.hasToken()) return false; 173 | 174 | var claims = service.allClaims(); 175 | 176 | var now = Date.now(); 177 | var issuedAtMSec = claims.iat * 1000; 178 | var expiresAtMSec = claims.exp * 1000; 179 | var marginMSec = 1000 * 60 * 5; // 5 Minutes 180 | 181 | // Substract margin, because browser time could be a bit in the past 182 | if (issuedAtMSec - marginMSec > now) { 183 | console.log('oidc-connect: Token is not yet valid!') 184 | return false 185 | } 186 | 187 | if (expiresAtMSec < now) { 188 | console.log('oidc-connect: Token has expired!') 189 | return false; 190 | } 191 | 192 | return true; 193 | } 194 | 195 | service.allClaims = function() { 196 | var cachedClaims = $localStorage['cached-claims']; 197 | 198 | if (!cachedClaims) { 199 | var id_token = service.getIdToken(); 200 | 201 | if (id_token) { 202 | var claims = service.convertToClaims(id_token); 203 | 204 | var idClaims = service.convertToClaims(id_token); 205 | $localStorage['cached-claims'] = idClaims; 206 | 207 | return claims; 208 | } 209 | } 210 | 211 | return cachedClaims; 212 | }; 213 | 214 | service.getIdToken = function() { 215 | return $localStorage['idToken']; 216 | }; 217 | 218 | service.clearTokens = function() { 219 | delete $localStorage['cached-claims']; 220 | delete $localStorage['idToken']; 221 | } 222 | }]); 223 | 224 | oidcmodule.provider("$auth", ['$routeProvider', function ($routeProvider) { 225 | 226 | // Default configuration 227 | var config = { 228 | basePath: null, 229 | clientId: null, 230 | apiUrl: '/api/', 231 | responseType: 'id_token', 232 | scope: "openid profile", 233 | redirectUri: (window.location.origin || window.location.protocol + '//' + window.location.host) + window.location.pathname + '#/auth/callback/', 234 | logoutUri: (window.location.origin || window.location.protocol + '//' + window.location.host) + window.location.pathname + '#/auth/clear', 235 | state: "", 236 | authorizationEndpoint: 'connect/authorize', 237 | revocationEndpoint: 'connect/revocation', 238 | endSessionEndpoint: 'connect/endsession', 239 | advanceRefresh: 300, 240 | enableRequestChecks: false, 241 | stickToLastKnownIdp: false 242 | }; 243 | 244 | return { 245 | 246 | // Service configuration 247 | configure: function (params) { 248 | angular.extend(config, params); 249 | }, 250 | 251 | // Service itself 252 | $get: ['$q', '$document', '$rootScope', '$localStorage', '$location', 'tokenService', function ($q, $document, $rootScope, $localStorage, $location, tokenService) { 253 | 254 | var init = function() { 255 | 256 | if ($localStorage['logoutActive']) { 257 | delete $localStorage['logoutActive']; 258 | 259 | tokenService.clearTokens(); 260 | } 261 | 262 | if ($localStorage['refreshRunning']) { 263 | delete $localStorage['refreshRunning']; 264 | } 265 | }; 266 | 267 | var createLoginUrl = function (nonce, state) { 268 | 269 | var hasPathDelimiter = config.basePath.endsWith('/'); 270 | var appendChar = (hasPathDelimiter) ? '' : '/'; 271 | 272 | var currentClaims = tokenService.allClaims(); 273 | if (currentClaims) { 274 | var idpClaimValue = currentClaims["idp"]; 275 | } 276 | 277 | var baseUrl = config.basePath + appendChar; 278 | var url = baseUrl + config.authorizationEndpoint 279 | + "?response_type=" 280 | + encodeURIComponent(config.responseType) 281 | + "&client_id=" 282 | + encodeURIComponent(config.clientId) 283 | + "&state=" 284 | + encodeURIComponent(state || config.state) 285 | + "&redirect_uri=" 286 | + encodeURIComponent(config.redirectUri) 287 | + "&scope=" 288 | + encodeURIComponent(config.scope) 289 | + "&nonce=" 290 | + encodeURIComponent(nonce); 291 | 292 | if (config.stickToLastKnownIdp && idpClaimValue) { 293 | url = url + "&acr_values=" 294 | + encodeURIComponent("idp:" + idpClaimValue); 295 | } 296 | 297 | return url; 298 | }; 299 | 300 | var createLogoutUrl = function(state) { 301 | 302 | var idToken = tokenService.getIdToken(); 303 | 304 | var hasPathDelimiter = config.basePath.endsWith('/'); 305 | var appendChar = (hasPathDelimiter) ? '' : '/'; 306 | 307 | var baseUrl = config.basePath + appendChar; 308 | var url = baseUrl + config.endSessionEndpoint 309 | + "?id_token_hint=" 310 | + encodeURIComponent(idToken) 311 | + "&post_logout_redirect_uri=" 312 | + encodeURIComponent(config.logoutUri) 313 | + "&state=" 314 | + encodeURIComponent(state || config.state) 315 | + "&r=" + Math.random(); 316 | return url; 317 | } 318 | 319 | var startImplicitFlow = function (localRedirect) { 320 | 321 | $localStorage['localRedirect'] = localRedirect; 322 | 323 | var url = createLoginUrl("dummynonce"); 324 | window.location.replace(url); 325 | }; 326 | 327 | var startLogout = function () { 328 | var url = createLogoutUrl(); 329 | $localStorage['logoutActive'] = true; 330 | 331 | window.location.replace(url); 332 | }; 333 | 334 | var handleImplicitFlowCallback = function(id_token) { 335 | 336 | tokenService.saveToken(id_token); 337 | 338 | var localRedirect = $localStorage['localRedirect']; 339 | 340 | if (localRedirect) { 341 | var redirectTo = localRedirect.hash.substring(1); 342 | delete $localStorage['localRedirect']; 343 | $location.path(redirectTo); 344 | } 345 | else { 346 | $location.path('/'); 347 | } 348 | 349 | $rootScope.$broadcast(loggedInEvent); 350 | return true; 351 | }; 352 | 353 | var handleSilentRefreshCallback = function(newIdToken) { 354 | 355 | delete $localStorage['refreshRunning']; 356 | 357 | var currentIdToken = tokenService.getIdToken(); 358 | var currentClaims = tokenService.allClaims(); 359 | 360 | var newClaims = tokenService.convertToClaims(newIdToken) 361 | 362 | if (currentClaims.exp && newClaims.exp && newClaims.exp > currentClaims.exp) { 363 | 364 | tokenService.saveToken(newIdToken); 365 | 366 | $rootScope.$broadcast(silentRefreshSuceededEvent); 367 | } 368 | else { 369 | $rootScope.$broadcast(silentRefreshFailedEvent); 370 | } 371 | }; 372 | 373 | var trySilentRefresh = function() { 374 | 375 | if ($localStorage['refreshRunning']) { 376 | return; 377 | } 378 | 379 | $localStorage['refreshRunning'] = true; 380 | 381 | $rootScope.$broadcast(silentRefreshStartedEvent); 382 | 383 | var url = createLoginUrl('dummynonce', 'refresh'); 384 | 385 | var html = ""; 386 | var elem = angular.element(html); 387 | 388 | $document.find("body").append(elem); 389 | 390 | setTimeout(function() { 391 | if ($localStorage['refreshRunning']) { 392 | $rootScope.$broadcast(silentRefreshTimeoutEvent); 393 | delete $localStorage['refreshRunning'] 394 | } 395 | 396 | $document.find("#oauthFrame").remove(); 397 | }, 5000); 398 | }; 399 | 400 | 401 | var handleSignInCallback = function(data) { 402 | 403 | if (!data && window.location.hash.indexOf("#") === 0) { 404 | data = window.location.hash.substr(16) 405 | } 406 | 407 | var fragments = {} 408 | if (data) { 409 | fragments = parseQueryString(data); 410 | } 411 | else { 412 | throw Error("Unable to process callback. No data given!"); 413 | } 414 | 415 | console.debug("oidc-angular: Processing callback information", data); 416 | 417 | var id_token = fragments['id_token']; 418 | var state = fragments['state']; 419 | 420 | if (id_token) { 421 | if (state === 'refresh') { 422 | handleSilentRefreshCallback(id_token); 423 | } 424 | else { 425 | handleImplicitFlowCallback(id_token); 426 | } 427 | } 428 | }; 429 | 430 | var handleSignOutCallback = function() { 431 | 432 | delete $localStorage['logoutActive']; 433 | 434 | tokenService.clearTokens(); 435 | $location.path('/'); 436 | 437 | $rootScope.$broadcast(loggedOutEvent); 438 | }; 439 | 440 | var tokenIsValidAt = function(date) { 441 | var claims = tokenService.allClaims(); 442 | 443 | var expiresAtMSec = claims.exp * 1000; 444 | 445 | if (date <= expiresAtMSec) { 446 | return true; 447 | } 448 | 449 | return false; 450 | } 451 | 452 | var validateExpirity = function() { 453 | if (!tokenService.hasToken()) return; 454 | if (!tokenService.hasValidToken()) return; 455 | 456 | var now = Date.now(); 457 | 458 | if (!tokenIsValidAt(now + config.advanceRefresh)) { 459 | $rootScope.$broadcast(tokenExpiresSoonEvent); 460 | trySilentRefresh(); 461 | } 462 | }; 463 | 464 | init(); 465 | 466 | return { 467 | config: config, 468 | 469 | handleSignInCallback : handleSignInCallback, 470 | 471 | handleSignOutCallback : handleSignOutCallback, 472 | 473 | validateExpirity: validateExpirity, 474 | 475 | isAuthenticated : function() { 476 | return tokenService.hasValidToken(); 477 | }, 478 | 479 | isAuthenticatedIn : function(milliseconds) { 480 | return tokenService.hasValidToken() && tokenIsValidAt(new Date().getTime() + milliseconds); 481 | }, 482 | 483 | signIn : function(localRedirect) { 484 | startImplicitFlow(localRedirect); 485 | }, 486 | 487 | signOut : function() { 488 | startLogout(); 489 | }, 490 | 491 | silentRefresh : function() { 492 | trySilentRefresh(); 493 | } 494 | 495 | }; 496 | }] 497 | }; 498 | }]); 499 | 500 | /* Helpers & Polyfills */ 501 | function parseQueryString(queryString) { 502 | var data = {}, pairs, pair, separatorIndex, escapedKey, escapedValue, key, value; 503 | 504 | if (queryString === null) { 505 | return data; 506 | } 507 | 508 | pairs = queryString.split("&"); 509 | 510 | for (var i = 0; i < pairs.length; i++) { 511 | pair = pairs[i]; 512 | separatorIndex = pair.indexOf("="); 513 | 514 | if (separatorIndex === -1) { 515 | escapedKey = pair; 516 | escapedValue = null; 517 | } else { 518 | escapedKey = pair.substr(0, separatorIndex); 519 | escapedValue = pair.substr(separatorIndex + 1); 520 | } 521 | 522 | key = decodeURIComponent(escapedKey); 523 | value = decodeURIComponent(escapedValue); 524 | 525 | if (key.substr(0, 1) === '/') 526 | key = key.substr(1); 527 | 528 | data[key] = value; 529 | } 530 | 531 | return data; 532 | }; 533 | 534 | 535 | if (!String.prototype.endsWith) { 536 | String.prototype.endsWith = function (searchString, position) { 537 | var subjectString = this.toString(); 538 | if (position === undefined || position > subjectString.length) { 539 | position = subjectString.length; 540 | } 541 | position -= searchString.length; 542 | var lastIndex = subjectString.indexOf(searchString, position); 543 | return lastIndex !== -1 && lastIndex === position; 544 | }; 545 | } 546 | 547 | if (!String.prototype.startsWith) { 548 | String.prototype.startsWith = function(searchString, position) { 549 | position = position || 0; 550 | return this.lastIndexOf(searchString, position) === position; 551 | }; 552 | } 553 | 554 | })(); 555 | -------------------------------------------------------------------------------- /sample/angular-base64.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /* 5 | * Encapsulation of Nick Galbreath's base64.js library for AngularJS 6 | * Original notice included below 7 | */ 8 | 9 | /* 10 | * Copyright (c) 2010 Nick Galbreath 11 | * http://code.google.com/p/stringencoders/source/browse/#svn/trunk/javascript 12 | * 13 | * Permission is hereby granted, free of charge, to any person 14 | * obtaining a copy of this software and associated documentation 15 | * files (the "Software"), to deal in the Software without 16 | * restriction, including without limitation the rights to use, 17 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | * copies of the Software, and to permit persons to whom the 19 | * Software is furnished to do so, subject to the following 20 | * conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 27 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 29 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 30 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 31 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 32 | * OTHER DEALINGS IN THE SOFTWARE. 33 | */ 34 | 35 | /* base64 encode/decode compatible with window.btoa/atob 36 | * 37 | * window.atob/btoa is a Firefox extension to convert binary data (the "b") 38 | * to base64 (ascii, the "a"). 39 | * 40 | * It is also found in Safari and Chrome. It is not available in IE. 41 | * 42 | * if (!window.btoa) window.btoa = base64.encode 43 | * if (!window.atob) window.atob = base64.decode 44 | * 45 | * The original spec's for atob/btoa are a bit lacking 46 | * https://developer.mozilla.org/en/DOM/window.atob 47 | * https://developer.mozilla.org/en/DOM/window.btoa 48 | * 49 | * window.btoa and base64.encode takes a string where charCodeAt is [0,255] 50 | * If any character is not [0,255], then an exception is thrown. 51 | * 52 | * window.atob and base64.decode take a base64-encoded string 53 | * If the input length is not a multiple of 4, or contains invalid characters 54 | * then an exception is thrown. 55 | */ 56 | 57 | angular.module('base64', []).constant('$base64', (function() { 58 | 59 | var PADCHAR = '='; 60 | 61 | var ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 62 | 63 | function getbyte64(s,i) { 64 | var idx = ALPHA.indexOf(s.charAt(i)); 65 | if (idx == -1) { 66 | throw "Cannot decode base64"; 67 | } 68 | return idx; 69 | } 70 | 71 | function decode(s) { 72 | // convert to string 73 | s = "" + s; 74 | var pads, i, b10; 75 | var imax = s.length; 76 | if (imax == 0) { 77 | return s; 78 | } 79 | 80 | if (imax % 4 != 0) { 81 | throw "Cannot decode base64"; 82 | } 83 | 84 | pads = 0; 85 | if (s.charAt(imax -1) == PADCHAR) { 86 | pads = 1; 87 | if (s.charAt(imax -2) == PADCHAR) { 88 | pads = 2; 89 | } 90 | // either way, we want to ignore this last block 91 | imax -= 4; 92 | } 93 | 94 | var x = []; 95 | for (i = 0; i < imax; i += 4) { 96 | b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) | 97 | (getbyte64(s,i+2) << 6) | getbyte64(s,i+3); 98 | x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff, b10 & 0xff)); 99 | } 100 | 101 | switch (pads) { 102 | case 1: 103 | b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) | (getbyte64(s,i+2) << 6); 104 | x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff)); 105 | break; 106 | case 2: 107 | b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12); 108 | x.push(String.fromCharCode(b10 >> 16)); 109 | break; 110 | } 111 | return x.join(''); 112 | } 113 | 114 | function getbyte(s,i) { 115 | var x = s.charCodeAt(i); 116 | if (x > 255) { 117 | throw "INVALID_CHARACTER_ERR: DOM Exception 5"; 118 | } 119 | return x; 120 | } 121 | 122 | function encode(s) { 123 | if (arguments.length != 1) { 124 | throw "SyntaxError: Not enough arguments"; 125 | } 126 | 127 | var i, b10; 128 | var x = []; 129 | 130 | // convert to string 131 | s = "" + s; 132 | 133 | var imax = s.length - s.length % 3; 134 | 135 | if (s.length == 0) { 136 | return s; 137 | } 138 | for (i = 0; i < imax; i += 3) { 139 | b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8) | getbyte(s,i+2); 140 | x.push(ALPHA.charAt(b10 >> 18)); 141 | x.push(ALPHA.charAt((b10 >> 12) & 0x3F)); 142 | x.push(ALPHA.charAt((b10 >> 6) & 0x3f)); 143 | x.push(ALPHA.charAt(b10 & 0x3f)); 144 | } 145 | switch (s.length - imax) { 146 | case 1: 147 | b10 = getbyte(s,i) << 16; 148 | x.push(ALPHA.charAt(b10 >> 18) + ALPHA.charAt((b10 >> 12) & 0x3F) + 149 | PADCHAR + PADCHAR); 150 | break; 151 | case 2: 152 | b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8); 153 | x.push(ALPHA.charAt(b10 >> 18) + ALPHA.charAt((b10 >> 12) & 0x3F) + 154 | ALPHA.charAt((b10 >> 6) & 0x3f) + PADCHAR); 155 | break; 156 | } 157 | return x.join(''); 158 | } 159 | 160 | return { 161 | encode: encode, 162 | decode: decode 163 | }; 164 | })()); 165 | 166 | })(); 167 | -------------------------------------------------------------------------------- /sample/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var browserSync = require('browser-sync'); 3 | 4 | gulp.task('default', ['serve']); 5 | 6 | gulp.task('serve', function() { 7 | browserSync.init({ 8 | server: { 9 | baseDir: "./", 10 | routes: { 11 | "/oidc-angular.js": "../oidc-angular.js" 12 | } 13 | }, 14 | 15 | open: false 16 | }); 17 | 18 | gulp.watch(["*.*", '../*.js']).on('change', browserSync.reload); 19 | }); 20 | -------------------------------------------------------------------------------- /sample/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 56 | 57 | -------------------------------------------------------------------------------- /sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oidc-angular-sample", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | --------------------------------------------------------------------------------