├── .gitignore ├── LICENSE ├── README.md ├── adal-angular.d.ts ├── adal.guard.ts ├── adal.interceptor.ts ├── adal.service.ts ├── gulpfile.js ├── index.ts ├── package-lock.json ├── package.json ├── tsconfig-aot.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | testem.log 35 | /typings 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ben Baran 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # adal-angular4 2 | [![Build status](https://ci.appveyor.com/api/projects/status/iwpwhyp6ymifxc4e?svg=true)](https://ci.appveyor.com/project/benbaran/adal-angular4) 3 | [![npm version](https://badge.fury.io/js/adal-angular4.svg)](https://badge.fury.io/js/adal-angular4) 4 | 5 | 6 | ___ 7 | 8 | Angular 4+ Adal wrapper package. Can be used to authenticate Angular applications against Azure Active Directory v1 endpoint. 9 | ___ 10 | 11 | ## Change Log 12 | 13 | ### 4.0.9 14 | 15 | - Upgraded to Gulp version 4. Build options are now: 16 | 17 | ``` 18 | gulp watch - watch for file changes do the build task 19 | gulp build - clean the dist directory and build the project 20 | gulp commit - bump version and add and commit files to git (for maintainers only) 21 | gulp publish - publish new npm version (for maintainers only) 22 | ``` 23 | ### 4.0.1 24 | 25 | - Updated to support latest version of adal-angular. Major version updated because of potentially breaking changes. 26 | 27 | ### 3.0.7 28 | 29 | - Added an automatic login token refresh feature. It will refresh tokens at application load if there is a valid sign-in token. It will also refresh the login token 5 minutes before it expires. 30 | 31 | ### 3.0.1 32 | 33 | - Updated to Angular 6, cleaned up files. THIS IS A BREAKING VERSION! 34 | 35 | ### 2.0.0 36 | 37 | - Updated to Angular 5, cleaned up files. THIS IS A BREAKING VERSION! 38 | 39 | ### 1.1.11 40 | 41 | - Fixed a bug where the valid scenario of refreshing an id_token is not handled - thanks to @alan-g-chen. 42 | 43 | ### 1.1.4 44 | 45 | - Hash is now removed from url after login 46 | 47 | ### 1.0.1 48 | 49 | - Added HTTP Interceptor for Angular 4.3.0+ 50 | - Updated all packages to newest versions 51 | 52 | ### Update and Build Instructions 53 | 54 | ``` 55 | git clone https://github.com/benbaran/adal-angular4.git 56 | 57 | npm install -g @angular/cli@latest gulp@latest 58 | 59 | del .\package-lock.json 60 | 61 | ng update --all --force 62 | 63 | npm install typescript@3.1.1 64 | 65 | gulp build 66 | ``` 67 | 68 | ### NPM Publish Instructions (For Maintainers Only) 69 | ``` 70 | git clone https://github.com/benbaran/adal-angular4.git 71 | 72 | npm install -g @angular/cli@latest gulp@latest 73 | 74 | del .\package-lock.json 75 | 76 | ng update --all --force 77 | 78 | npm install typescript@3.1.1 79 | 80 | gulp publish 81 | ``` 82 | 83 | ### Usage ( tested with Angular 8) 84 | 85 | First install the package ( ex using npm ) 86 | ``` 87 | npm i adal-angular4 88 | ``` 89 | 90 | Implement ADAL authentication: 91 | 92 | ## app.module.ts 93 | Open your Angular root module, usually app.module.ts 94 | Add the following import 95 | 96 | ```Javascript 97 | /*Authentication*/ 98 | import { AdalService, AdalGuard, AdalInterceptor } from 'adal-angular4'; 99 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 100 | ``` 101 | 102 | Update your @NgModule providers section with the following line: 103 | ```JSON 104 | providers: [AdalService, AdalGuard, {provide: HTTP_INTERCEPTORS, useClass: AdalInterceptor, multi: true } 105 | ``` 106 | 107 | It should look something like this: 108 | ```Javascript 109 | @NgModule({ 110 | declarations: [ 111 | AppComponent, 112 | ... 113 | ], 114 | imports: [ 115 | ... 116 | ], 117 | providers: [AdalService, AdalGuard, {provide: HTTP_INTERCEPTORS, useClass: AdalInterceptor, multi: true }, ... ], 118 | bootstrap: [AppComponent] 119 | }) 120 | ``` 121 | ## environment.ts 122 | inside your environment.ts file add your config, this file is created for you when u use the Angular CLI 123 | 124 | Usually can be found here: 125 | src 126 | ├── environements 127 | └── environment.ts 128 | └── environment.prod.ts 129 | 130 | 131 | ```Javascript 132 | export const environment = { 133 | production: false, 134 | config: { 135 | tenant: 'tenant.onmicrosoft.com', 136 | clientId: 'app registration id', 137 | endpoints: { 138 | 'http://localhost:4200/': 'the id' 139 | } 140 | } 141 | }; 142 | ``` 143 | ## app.component.ts 144 | Then import the following into your root component usually app.component.ts 145 | 146 | ```Javascript 147 | import { environment } from '../environments/environment'; 148 | import { AdalService } from 'adal-angular4'; 149 | ``` 150 | 151 | Init the adal lib by adding the following lines to your constructor or OnInit 152 | 153 | ```Javascript 154 | export class AppComponent implements { 155 | constructor(private adalService: AdalService) { 156 | this.adalService.init(environment.config); 157 | this.adalService.handleWindowCallback(); 158 | } 159 | } 160 | ``` 161 | 162 | ## app-routing.module.ts 163 | You can protect routes by adding the authguard to you route 164 | 165 | import the following inside your routing module file usually app-routing.module.ts 166 | ```Javascript 167 | import { AdalGuard } from 'adal-angular4'; 168 | ``` 169 | 170 | 171 | ex: 172 | ```Javascript 173 | const routes: Routes = [ 174 | { 175 | path: '', 176 | component: YourComponent, 177 | canActivate: [AdalGuard] 178 | } 179 | ]; 180 | ``` 181 | 182 | ## Login / Logout 183 | You can call the login and logout function with the following code. 184 | 185 | AdalService needs to be imported in your file 186 | ```Javascript 187 | import { AdalService } from 'adal-angular4'; 188 | /*Dont forget to initialize*/ 189 | constructor(private adalService: AdalService) 190 | ``` 191 | 192 | **Login:** 193 | ```Javascript 194 | this.adalService.login(); 195 | ``` 196 | 197 | **Logout:** 198 | ```Javascript 199 | this.adalService.logOut(); 200 | ``` 201 | 202 | ### Check if user is allready authenticated, if not login 203 | Checking if user has allready logged in if not do something 204 | 205 | AdalService needs to be imported in your file 206 | ```Javascript 207 | import { AdalService } from 'adal-angular4'; 208 | /*Dont forget to initialize*/ 209 | constructor(private adalService: AdalService) 210 | ``` 211 | 212 | ```Javascript 213 | if (this.adalService.userInfo.authenticated) { 214 | /*All good*/ 215 | } else { 216 | /*No good*/ 217 | } 218 | ``` 219 | 220 | ## Using Azure AD Role based Authentication 221 | This section is optional, the library works without these settings aswell. 222 | 223 | Setup for Role based authentication in your Angular application: 224 | 225 | ### Steps to do in Azure 226 | Go to you app registration, the same one you used the clientId from inside your environments file. 227 | And add your custom roles inside the app manifest: 228 | 229 | ```Javascript 230 | "appRoles": [ 231 | { 232 | "allowedMemberTypes": [ 233 | "User" 234 | ], 235 | "description": "your user description", 236 | "displayName": "UserRole", 237 | "id": "generate a unique guid", 238 | "isEnabled": true, 239 | "value": "UserRole" 240 | }, 241 | { 242 | "allowedMemberTypes": [ 243 | "User" 244 | ], 245 | "description": "your admin description", 246 | "displayName": "AdminRole", 247 | "id": "generate a unique guid", 248 | "isEnabled": true, 249 | "value": "AdminRole" 250 | } 251 | ] 252 | ``` 253 | 254 | Once these roles have been created you can assign them to users: 255 | 256 | Open the registration for local directory, this can be found on the overview page on the right side 'Managed application in local directory: -your app registration name-' 257 | On this page go to 'Users and groups' and click 'Add user' 258 | 259 | Now add the user that needs to be able to access the application and assign a role to the user. 260 | 261 | The last step in Azure is activating the User assignment required property in the 'Properties' tab. 262 | When you enable this option only users that are assigned a role can login to the application. 263 | 264 | ### Steps in Angular 265 | In order to protect a certain route to be accessed only by authenticated users with a certain role: 266 | Update the 267 | #### app-routing.module.ts 268 | with the following data attribute 269 | 270 | 271 | ```Javascript 272 | const routes: Routes = [ 273 | { 274 | path: '', 275 | component: YourComponent, 276 | canActivate: [AdalGuard], 277 | data: { expectedRole: 'AdminRole' } 278 | } 279 | ]; 280 | ``` 281 | expectedRole can be any of the custom Roles you created in App manifest (appRoles) for your app registration in Azure. 282 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /adal-angular.d.ts: -------------------------------------------------------------------------------- 1 | 2 | // Typings needed for using ADAL with Angular 4 3 | declare module 'adal-angular' { 4 | export function inject(config: adal.Config): adal.AuthenticationContext; 5 | 6 | export class adal{ 7 | 8 | } 9 | } 10 | 11 | declare var AuthenticationContext: adal.AuthenticationContextStatic; 12 | declare var Logging: adal.Logging; 13 | 14 | declare namespace adal { 15 | 16 | /** 17 | * 18 | * 19 | * @interface Config 20 | */ 21 | interface Config { 22 | tenant?: string; 23 | clientId: string; 24 | redirectUri?: string; 25 | instance?: string; 26 | endpoints?: any; // If you need to send CORS api requests. 27 | popUp?: boolean; 28 | localLoginUrl?: string; 29 | displayCall?: (urlNavigate: string) => any; 30 | postLogoutRedirectUri?: string; // redirect url after succesful logout operation 31 | cacheLocation?: string; 32 | anonymousEndpoints?: string[]; 33 | expireOffsetSeconds?: number; 34 | correlationId?: string; 35 | loginResource?: string; 36 | resource?: string; 37 | extraQueryParameter?: string; 38 | navigateToLoginRequestUrl?: boolean; 39 | logOutUri?: string; 40 | loadFrameTimeout?: number; 41 | } 42 | 43 | /** 44 | * 45 | * 46 | * @interface User 47 | */ 48 | interface User { 49 | userName: string; 50 | profile: any; 51 | authenticated: any; 52 | error: any; 53 | token: any; 54 | loginCached: boolean; 55 | } 56 | 57 | /** 58 | * 59 | * 60 | * @interface RequestInfo 61 | */ 62 | interface RequestInfo { 63 | valid: boolean; 64 | parameters: any; 65 | stateMatch: boolean; 66 | stateResponse: string; 67 | requestType: string; 68 | } 69 | 70 | /** 71 | * 72 | * 73 | * @interface Logging 74 | */ 75 | interface Logging { 76 | log: (message: string) => void; 77 | level: LoggingLevel; 78 | } 79 | 80 | /** 81 | * 82 | * 83 | * @enum {number} 84 | */ 85 | enum LoggingLevel { 86 | ERROR = 0, 87 | WARNING = 1, 88 | INFO = 2, 89 | VERBOSE = 3 90 | } 91 | 92 | /** 93 | * 94 | * 95 | * @interface AuthenticationContextStatic 96 | */ 97 | interface AuthenticationContextStatic { 98 | new (config: Config): AuthenticationContext; 99 | } 100 | 101 | /** 102 | * 103 | * 104 | * @interface AuthenticationContext 105 | */ 106 | interface AuthenticationContext { 107 | 108 | // Additional items for Angular 4 109 | CONSTANTS: any; 110 | 111 | REQUEST_TYPE: { 112 | LOGIN: string, 113 | RENEW_TOKEN: string, 114 | UNKNOWN: string 115 | }; 116 | 117 | // Methods 118 | callback: any; 119 | 120 | _getItem: any; 121 | 122 | _renewFailed: any; 123 | 124 | _openedWindows: any; 125 | 126 | _callBackMappedToRenewStates: any; 127 | 128 | // Original ADAL Types 129 | instance: string; 130 | config: Config; 131 | 132 | /** 133 | * Gets initial Idtoken for the app backend 134 | * Saves the resulting Idtoken in localStorage. 135 | */ 136 | login(): void; 137 | 138 | /** 139 | * Indicates whether login is in progress now or not. 140 | */ 141 | loginInProgress(): boolean; 142 | 143 | /** 144 | * Gets token for the specified resource from local storage cache 145 | * @param {string} resource A URI that identifies the resource for which the token is valid. 146 | * @returns {string} token if exists and not expired or null 147 | */ 148 | getCachedToken(resource: string): string; 149 | 150 | /** 151 | * Retrieves and parse idToken from localstorage 152 | * @returns {User} user object 153 | */ 154 | getCachedUser(): User; 155 | 156 | registerCallback(expectedState: string, resource: string, callback: (message: string, token: string) => any): void; 157 | 158 | /** 159 | * Acquire token from cache if not expired and available. Acquires token from iframe if expired. 160 | * @param {string} resource ResourceUri identifying the target resource 161 | * @param {requestCallback} callback 162 | */ 163 | acquireToken(resource: string, callback: (message: string, token: string) => any): void; 164 | 165 | /** 166 | * Redirect the Browser to Azure AD Authorization endpoint 167 | * @param {string} urlNavigate The authorization request url 168 | */ 169 | promptUser(urlNavigate: string): void; 170 | 171 | /** 172 | * Clear cache items. 173 | */ 174 | clearCache(): void; 175 | 176 | /** 177 | * Clear cache items for a resource. 178 | */ 179 | clearCacheForResource(resource: string): void; 180 | 181 | /** 182 | * Logout user will redirect page to logout endpoint. 183 | * After logout, it will redirect to post_logout page if provided. 184 | */ 185 | logOut(): void; 186 | 187 | /** 188 | * Gets a user profile 189 | * @param {requestCallback} callback - The callback that handles the response. 190 | */ 191 | getUser(callback: (message: string, user?: User) => any): void; 192 | 193 | /** 194 | * Checks if hash contains access token or id token or error_description 195 | * @param {string} hash - Hash passed from redirect page 196 | * @returns {Boolean} 197 | */ 198 | isCallback(hash: string): boolean; 199 | 200 | /** 201 | * Gets login error 202 | * @returns {string} error message related to login 203 | */ 204 | getLoginError(): string; 205 | 206 | /** 207 | * Gets requestInfo from given hash. 208 | * @returns {RequestInfo} for appropriate hash. 209 | */ 210 | getRequestInfo(hash: string): RequestInfo; 211 | 212 | /** 213 | * Saves token from hash that is received from redirect. 214 | */ 215 | saveTokenFromHash(requestInfo: RequestInfo): void; 216 | 217 | /** 218 | * Gets resource for given endpoint if mapping is provided with config. 219 | * @param {string} endpoint - API endpoint 220 | * @returns {string} resource for this API endpoint 221 | */ 222 | getResourceForEndpoint(endpoint: string): string; 223 | 224 | /** 225 | * Handles redirection after login operation. 226 | * Gets access token from url and saves token to the (local/session) storage 227 | * or saves error in case unsuccessful login. 228 | * @param {boolean} removeHash - Set to false if you use HashLocationStrategy to retain URL after refresh 229 | */ 230 | handleWindowCallback(removeHash: boolean): void; 231 | 232 | log(level: number, message: string, error: any): void; 233 | error(message: string, error: any): void; 234 | warn(message: string): void; 235 | info(message: string): void; 236 | verbose(message: string): void; 237 | } 238 | 239 | } 240 | 241 | /** 242 | * 243 | * 244 | * @interface Window 245 | */ 246 | interface Window { 247 | _adalInstance: adal.AuthenticationContext; 248 | } 249 | -------------------------------------------------------------------------------- /adal.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { AdalService } from './adal.service'; 5 | 6 | @Injectable() 7 | export class AdalGuard implements CanActivate, CanActivateChild { 8 | 9 | constructor(private adalService: AdalService) { } 10 | 11 | canActivate( 12 | route: ActivatedRouteSnapshot, 13 | state: RouterStateSnapshot 14 | ): Observable | Promise | boolean { 15 | 16 | const expectedRole = route.data.expectedRole; 17 | 18 | if(expectedRole){ 19 | return (this.adalService.userInfo.authenticated && this.adalService.userInfo.profile.roles.includes(expectedRole)); 20 | } else { 21 | return this.adalService.userInfo.authenticated; 22 | } 23 | 24 | } 25 | 26 | public canActivateChild( 27 | childRoute: ActivatedRouteSnapshot, 28 | state: RouterStateSnapshot 29 | ): Observable | Promise | boolean { 30 | return this.canActivate(childRoute, state); 31 | } 32 | } -------------------------------------------------------------------------------- /adal.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { mergeMap } from 'rxjs/operators'; 5 | 6 | import { AdalService } from './adal.service'; 7 | 8 | @Injectable() 9 | export class AdalInterceptor implements HttpInterceptor { 10 | 11 | constructor(private adal: AdalService) { } 12 | 13 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 14 | 15 | // if the endpoint is not registered 16 | // or if the header 'skip-adal' is set 17 | // then pass the request as it is to the next handler 18 | const resource = this.adal.getResourceForEndpoint(request.url); 19 | const skipAdal = request.headers.get('skip-adal'); 20 | if (!resource || skipAdal) { 21 | return next.handle(request); 22 | } 23 | 24 | // if the user is not authenticated then drop the request 25 | if (!this.adal.userInfo.authenticated) { 26 | throw new Error('Cannot send request to registered endpoint if the user is not authenticated.'); 27 | } 28 | 29 | // if the endpoint is registered then acquire and inject token 30 | return this.adal.acquireToken(resource) 31 | .pipe( 32 | mergeMap((token: string) => { 33 | // clone the request and replace the original headers with 34 | // cloned headers, updated with the authorization 35 | const authorizedRequest = request.clone({ 36 | headers: request.headers.set('Authorization', 'Bearer ' + token), 37 | }); 38 | 39 | return next.handle(authorizedRequest); 40 | } 41 | ) 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /adal.service.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { Injectable, NgZone } from '@angular/core'; 4 | 5 | import { Observable, bindCallback, timer } from 'rxjs'; 6 | import { map } from 'rxjs/operators'; 7 | 8 | import * as lib from 'adal-angular'; 9 | 10 | @Injectable() 11 | export class AdalService { 12 | 13 | 14 | private context: adal.AuthenticationContext = null; 15 | private loginRefreshTimer = null; 16 | 17 | 18 | private user: adal.User = { 19 | authenticated: false, 20 | userName: '', 21 | error: '', 22 | token: '', 23 | profile: {}, 24 | loginCached: false 25 | }; 26 | 27 | constructor(private ngZone: NgZone) { } 28 | 29 | public init(configOptions: adal.Config): void { 30 | if (!configOptions) { 31 | throw new Error('You must set config, when calling init.'); 32 | } 33 | 34 | // redirect and logout_redirect are set to current location by default 35 | const existingHash = window.location.hash; 36 | 37 | let pathDefault = window.location.href; 38 | if (existingHash) { 39 | pathDefault = pathDefault.replace(existingHash, ''); 40 | } 41 | 42 | configOptions.redirectUri = configOptions.redirectUri || pathDefault; 43 | configOptions.postLogoutRedirectUri = configOptions.postLogoutRedirectUri || pathDefault; 44 | 45 | // create instance with given config 46 | this.context = lib.inject(configOptions); 47 | 48 | this.updateDataFromCache(); 49 | 50 | if (this.user.loginCached && !this.user.authenticated && window.self == window.top && !this.isInCallbackRedirectMode) { 51 | this.refreshLoginToken(); 52 | } else if (this.user.loginCached && this.user.authenticated && !this.loginRefreshTimer && window.self == window.top) { 53 | this.setupLoginTokenRefreshTimer(); 54 | } 55 | 56 | } 57 | 58 | public get config(): adal.Config { 59 | return this.context.config; 60 | } 61 | 62 | public get userInfo(): adal.User { 63 | return this.user; 64 | } 65 | 66 | public login(): void { 67 | this.context.login(); 68 | } 69 | 70 | public loginInProgress(): boolean { 71 | return this.context.loginInProgress(); 72 | } 73 | 74 | public logOut(): void { 75 | this.context.logOut(); 76 | } 77 | 78 | public handleWindowCallback(removeHash: boolean = true): void { 79 | const hash = window.location.hash; 80 | if (this.context.isCallback(hash)) { 81 | var isPopup = false; 82 | 83 | if (this.context._openedWindows.length > 0 && this.context._openedWindows[this.context._openedWindows.length - 1].opener && this.context._openedWindows[this.context._openedWindows.length - 1].opener._adalInstance) { 84 | this.context = this.context._openedWindows[this.context._openedWindows.length - 1].opener._adalInstance; 85 | isPopup = true; 86 | } else { 87 | try { 88 | if (window.parent && window.parent._adalInstance) { 89 | this.context = window.parent._adalInstance; 90 | } 91 | } catch { 92 | // ignore any exceptions and resort to default context. 93 | } 94 | } 95 | 96 | const requestInfo = this.context.getRequestInfo(hash); 97 | this.context.saveTokenFromHash(requestInfo); 98 | var callback = this.context._callBackMappedToRenewStates[requestInfo.stateResponse] || this.context.callback; 99 | 100 | if (requestInfo.requestType === this.context.REQUEST_TYPE.LOGIN) { 101 | this.updateDataFromCache(); 102 | this.setupLoginTokenRefreshTimer(); 103 | } else if (requestInfo.requestType === this.context.REQUEST_TYPE.RENEW_TOKEN) { 104 | this.updateDataFromCache(); 105 | } 106 | 107 | if (requestInfo.stateMatch) { 108 | if (typeof callback === 'function') { 109 | if (requestInfo.requestType === this.context.REQUEST_TYPE.RENEW_TOKEN) { 110 | // Idtoken or Accestoken can be renewed 111 | if (requestInfo.parameters['access_token']) { 112 | callback(this.context._getItem(this.context.CONSTANTS.STORAGE.ERROR_DESCRIPTION) 113 | , requestInfo.parameters['access_token']); 114 | } else if (requestInfo.parameters['id_token']) { 115 | callback(this.context._getItem(this.context.CONSTANTS.STORAGE.ERROR_DESCRIPTION) 116 | , requestInfo.parameters['id_token']); 117 | } else if (requestInfo.parameters['error']) { 118 | callback(this.context._getItem(this.context.CONSTANTS.STORAGE.ERROR_DESCRIPTION), null); 119 | this.context._renewFailed = true; 120 | } 121 | } 122 | } 123 | } 124 | } 125 | 126 | // Remove hash from url 127 | if (removeHash) { 128 | if (window.location.hash) { 129 | if (window.history.replaceState) { 130 | window.history.replaceState('', '/', window.location.pathname) 131 | } else { 132 | window.location.hash = ''; 133 | } 134 | } 135 | } 136 | } 137 | 138 | public getCachedToken(resource: string): string | null { 139 | return this.context.getCachedToken(resource); 140 | } 141 | 142 | public acquireToken(resource: string): Observable { 143 | return bindCallback((callback) => { 144 | this.context.acquireToken(resource, (error: string, tokenOut: string) => { 145 | if (error) { 146 | this.context.error('Error when acquiring token for resource: ' + resource, error); 147 | callback(null, error); 148 | } else { 149 | callback(tokenOut, null); 150 | } 151 | }); 152 | })() 153 | .pipe( 154 | map((result) => { 155 | if (!result[0] && result[1]) { 156 | throw (result[1]); 157 | } 158 | 159 | return result[0]; 160 | }) 161 | ); 162 | } 163 | 164 | public getUser(): Observable { 165 | return bindCallback((callback) => { 166 | this.context.getUser( (error: string, user?: adal.User) => { 167 | if (error) { 168 | this.context.error('Error when getting user', error); 169 | callback(null); 170 | } else { 171 | callback(user || null); 172 | } 173 | }); 174 | })(); 175 | } 176 | 177 | public clearCache(): void { 178 | this.context.clearCache(); 179 | } 180 | 181 | public clearCacheForResource(resource: string): void { 182 | this.context.clearCacheForResource(resource); 183 | } 184 | 185 | public info(message: string): void { 186 | this.context.info(message); 187 | } 188 | 189 | public verbose(message: string): void { 190 | this.context.verbose(message); 191 | } 192 | 193 | public getResourceForEndpoint(url: string): string | null { 194 | return this.context 195 | ? null 196 | : this.context.getResourceForEndpoint(url); 197 | } 198 | 199 | 200 | public refreshDataFromCache() { 201 | this.updateDataFromCache(); 202 | 203 | } 204 | 205 | private updateDataFromCache(): void { 206 | const token = this.context.getCachedToken(this.context.config.loginResource); 207 | this.user.authenticated = token !== null && token.length > 0; 208 | 209 | const user = this.context.getCachedUser(); 210 | 211 | if (user) { 212 | this.user.userName = user.userName; 213 | this.user.profile = user.profile; 214 | this.user.token = token; 215 | this.user.error = this.context.getLoginError(); 216 | this.user.loginCached = true; 217 | } else { 218 | this.user.userName = ''; 219 | this.user.profile = {}; 220 | this.user.token = ''; 221 | this.user.error = this.context.getLoginError(); 222 | this.user.loginCached = false; 223 | } 224 | } 225 | 226 | private refreshLoginToken(): void { 227 | if (!this.user.loginCached) throw ("User not logged in"); 228 | this.acquireToken(this.context.config.loginResource).subscribe((token: string) => { 229 | this.user.token = token; 230 | if (this.user.authenticated == false) { 231 | this.user.authenticated = true; 232 | this.user.error = ''; 233 | window.location.reload(); 234 | } else { 235 | this.setupLoginTokenRefreshTimer(); 236 | } 237 | }, (error: string) => { 238 | this.user.authenticated = false; 239 | this.user.error = this.context.getLoginError(); 240 | }); 241 | } 242 | 243 | private now(): number { 244 | return Math.round(new Date().getTime() / 1000.0); 245 | } 246 | 247 | private get isInCallbackRedirectMode(): boolean { 248 | return window.location.href.indexOf("#access_token") !== -1 || window.location.href.indexOf("#id_token") !== -1; 249 | }; 250 | 251 | private setupLoginTokenRefreshTimer(): void { 252 | // Get expiration of login token 253 | let exp = this.context._getItem(this.context.CONSTANTS.STORAGE.EXPIRATION_KEY + this.context.config.loginResource); 254 | 255 | // Either wait until the refresh window is valid or refresh in 1 second (measured in seconds) 256 | let timerDelay = exp - this.now() - (this.context.config.expireOffsetSeconds || 300) > 0 ? exp - this.now() - (this.context.config.expireOffsetSeconds || 300) : 1; 257 | if (this.loginRefreshTimer) this.loginRefreshTimer.unsubscribe(); 258 | 259 | this.ngZone.runOutsideAngular(() => { 260 | this.loginRefreshTimer = timer(timerDelay * 1000).subscribe((x) => { 261 | this.refreshLoginToken() 262 | }); 263 | }); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const { series } = require('gulp'); 2 | 3 | var 4 | bump = require('gulp-bump'), 5 | del = require('del'), 6 | exec = require('child_process').exec, 7 | gulp = require('gulp'), 8 | replace = require('gulp-replace'), 9 | watch = require('gulp-watch'), 10 | fs = require('fs'); 11 | 12 | // watch for changes files and recompile 13 | function watch_files() { 14 | gulp.watch("*", gulp.series(clean, compile)); 15 | } 16 | 17 | // 1. delete contents of dist directory 18 | function clean(cb) { 19 | del(['./dist/*', '!dist/index.js']); 20 | cb(); 21 | } 22 | 23 | // 2. compile to dist directory 24 | function compile(cb) { 25 | exec('npm run compile', function (err, stdout, stderr) { 26 | console.log(stdout); 27 | console.log(stderr); 28 | cb(err); 29 | }) 30 | } 31 | 32 | // 3. include package.json file in ./dist folder 33 | function package(cb) { 34 | const pkgjson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 35 | delete pkgjson.scripts; 36 | delete pkgjson.devDependencies; 37 | const filepath = './dist/package.json'; 38 | fs.writeFileSync(filepath, JSON.stringify(pkgjson, null, 2), 'utf-8'); 39 | console.log('package.json Copied to Dist Directory'); 40 | cb(); 41 | } 42 | 43 | // 4. include type definition file for adal-angular 44 | function copy(cb) { 45 | gulp.src(['adal-angular.d.ts']).pipe(gulp.dest('./dist/')); 46 | console.log('adal-angular.d.ts Copied to Dist Directory'); 47 | 48 | gulp.src(['README.md']).pipe(gulp.dest('./dist/')); 49 | console.log('README.md Copied to Dist Directory'); 50 | 51 | cb(); 52 | } 53 | 54 | // 5. rewrite type definition file path for adal-angular in adal.service.d.ts 55 | function replace_d(cb) { 56 | gulp.src('./dist/adal.service.d.ts') 57 | .pipe(replace('../adal-angular.d.ts', './adal-angular.d.ts')) 58 | .pipe(gulp.dest('./dist/')); 59 | console.log('adal.service.d.ts Path Updated'); 60 | cb(); 61 | } 62 | 63 | // 6. increase the version in package.json 64 | function bump_version(cb) { 65 | gulp.src('./package.json') 66 | .pipe(bump({ 67 | type: 'patch' 68 | })) 69 | .pipe(gulp.dest('./')); 70 | console.log('Version Bumped'); 71 | cb(); 72 | } 73 | 74 | // 7. git add 75 | function git_add(cb) { 76 | exec('git add -A', function (err, stdout, stderr) { 77 | console.log(stdout); 78 | console.log(stderr); 79 | cb(err); 80 | }); 81 | } 82 | 83 | // 8. git commit 84 | function git_commit(cb) { 85 | var package = require('./package.json'); 86 | exec('git commit -m "Version ' + package.version + ' release."', function (err, stdout, stderr) { 87 | console.log(stdout); 88 | console.log(stderr); 89 | cb(err); 90 | }); 91 | } 92 | 93 | // 9. git push 94 | function git_push(cb) { 95 | exec('git push', function (err, stdout, stderr) { 96 | console.log(stdout); 97 | console.log(stderr); 98 | cb(err); 99 | }); 100 | } 101 | 102 | 103 | // 10. publish ./dist directory to npm 104 | function npm_publish(cb) { 105 | exec('npm publish ./dist', function (err, stdout, stderr) { 106 | console.log(stdout); 107 | console.log(stderr); 108 | cb(err); 109 | }); 110 | } 111 | 112 | // Gulp Tasks 113 | 114 | exports.watch = series(watch_files); 115 | 116 | exports.build = series(clean, compile, package, copy, replace_d); 117 | 118 | exports.commit = series(clean, compile, package, copy, replace_d, bump_version, git_add, git_commit, git_push); 119 | 120 | exports.publish = series(clean, compile, package, copy, replace_d, bump_version, git_add, git_commit, git_push, npm_publish); 121 | 122 | 123 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export { AdalService } from './adal.service'; 2 | export { AdalGuard } from './adal.guard'; 3 | export { AdalInterceptor } from './adal.interceptor'; 4 | 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adal-angular4", 3 | "version": "4.0.13", 4 | "description": "Angular 4+ ADAL Wrapper", 5 | "main": "index.js", 6 | "scripts": { 7 | "compile": "node_modules/.bin/ngc -p tsconfig-aot.json" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/benbaran/adal-angular4.git" 12 | }, 13 | "keywords": [ 14 | "ADAL", 15 | "Angular" 16 | ], 17 | "author": "Ben Baran", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/benbaran/adal-angular4/issues" 21 | }, 22 | "homepage": "https://github.com/benbaran/adal-angular4#readme", 23 | "peerDependencies": {}, 24 | "devDependencies": { 25 | "@angular/common": "~7.1.3", 26 | "@angular/compiler": "~7.1.3", 27 | "@angular/compiler-cli": "~7.1.3", 28 | "@angular/core": "~7.1.3", 29 | "@angular/platform-browser": "~7.1.3", 30 | "@angular/router": "~7.1.3", 31 | "@types/jasmine": "~3.3.2", 32 | "del": "^3.0.0", 33 | "gulp": "^4.0.0", 34 | "gulp-bump": "^3.1.1", 35 | "gulp-replace": "^1.0.0", 36 | "gulp-watch": "^5.0.0", 37 | "npm": "^6.4.0", 38 | "rollup": "~0.68.0", 39 | "rxjs": "~6.3.3", 40 | "typescript": "^3.1.1", 41 | "uglify-js": "~3.4.8", 42 | "zone.js": "~0.8.26" 43 | }, 44 | "dependencies": { 45 | "adal-angular": "^1.0.17" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig-aot.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "removeComments": false, 9 | "noImplicitAny": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "allowUnreachableCode": false, 13 | "allowUnusedLabels": false, 14 | "pretty": true, 15 | "stripInternal": true, 16 | "skipLibCheck": true, 17 | "outDir": "dist", 18 | "rootDir": "./" 19 | }, 20 | "files": [ 21 | "./index.ts" 22 | ], 23 | "angularCompilerOptions": { 24 | "genDir": "dist", 25 | "debug": false, 26 | "skipTemplateCodegen": true, 27 | "skipMetadataEmit": false, 28 | "strictMetadataEmit": true 29 | } 30 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "declaration": true, 5 | "stripInternal": true, 6 | "experimentalDecorators": true, 7 | "strictNullChecks": true, 8 | "noImplicitAny": true, 9 | "module": "es2015", 10 | "moduleResolution": "node", 11 | "paths": { 12 | "@angular/core": [ 13 | "node_modules/@angular/core" 14 | ], 15 | "rxjs/*": [ 16 | "node_modules/rxjs/*" 17 | ] 18 | }, 19 | "rootDir": ".", 20 | "outDir": "dist", 21 | "sourceMap": true, 22 | "inlineSources": true, 23 | "target": "es5", 24 | "skipLibCheck": true, 25 | "lib": [ 26 | "es2015", 27 | "dom" 28 | ] 29 | }, 30 | "files": [ 31 | "index.ts" 32 | ], 33 | "angularCompilerOptions": { 34 | "strictMetadataEmit": true, 35 | "disableTypeScriptVersionCheck": true 36 | } 37 | } --------------------------------------------------------------------------------