42 | This is an example for Angular-Token. Angular-Token is a token based authentication service for Angular with
43 | multiple user support. It works best with the devise token auth gem for Rails.
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/app/example/example.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { FormsModule } from '@angular/forms';
3 | import { CommonModule } from '@angular/common';
4 | import { RouterModule } from '@angular/router';
5 | import { MatInputModule } from '@angular/material/input';
6 | import { MatButtonModule } from '@angular/material/button';
7 | import { MatCardModule } from '@angular/material/card';
8 | import { MatDividerModule } from '@angular/material/divider';
9 |
10 | import { MatIconModule } from '@angular/material/icon';
11 |
12 | import { ExampleComponent } from './example.component';
13 | import { OutputComponent } from './output/output.component';
14 | import { RegisterComponent } from './register/register.component';
15 | import { SignInComponent } from './sign-in/sign-in.component';
16 | import { ChangePasswordComponent } from './change-password/change-password.component';
17 | import { AccessResourceComponent } from './access-resource/access-resource.component';
18 | import { ValidateTokenComponent } from './validate-token/validate-token.component';
19 | import { SignOutComponent } from './sign-out/sign-out.component';
20 | import { CanActivateComponent } from './can-activate/can-activate.component';
21 |
22 | @NgModule({
23 | imports: [
24 | CommonModule,
25 | FormsModule,
26 | RouterModule,
27 |
28 | MatInputModule,
29 | MatButtonModule,
30 | MatCardModule,
31 | MatIconModule,
32 | MatDividerModule
33 | ],
34 | declarations: [
35 | ExampleComponent,
36 | OutputComponent,
37 | RegisterComponent,
38 | SignInComponent,
39 | ChangePasswordComponent,
40 | SignOutComponent,
41 | AccessResourceComponent,
42 | ValidateTokenComponent,
43 | CanActivateComponent
44 | ],
45 | exports: [
46 | ExampleComponent
47 | ]
48 | })
49 | export class ExampleModule { }
50 |
--------------------------------------------------------------------------------
/docs/multiple-user-types.md:
--------------------------------------------------------------------------------
1 | An array of `UserType` can be passed in `AngularTokenOptions` at the root module with`.forRoot()`.
2 | The user type is selected during sign in and persists until sign out.
3 | `.currentUserType()` returns the currently logged in user.
4 |
5 | #### Example:
6 | ```javascript
7 | this.tokenService.init({
8 | userTypes: [
9 | { name: 'ADMIN', path: 'admin' },
10 | { name: 'USER', path: 'user' }
11 | ]
12 | });
13 |
14 | this.tokenService.signIn({
15 | login: 'example@example.com',
16 | password: 'secretPassword',
17 | userType: 'ADMIN'
18 | })
19 |
20 | this.tokenService.currentUserType; // ADMIN
21 | ```
22 |
23 | ## Showing/Hiding Elements based on UserType
24 | When wanting to show or hide certain elements based on the UserRole, the following directive can be used as guideline.
25 |
26 | ```javascript
27 | import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
28 | import { AngularTokenService } from 'angular-token';
29 |
30 | @Directive({
31 | selector: '[ifInRole]'
32 | })
33 | export class IfInRoleDirective implements OnInit {
34 | @Input('ifInRole') role: string;
35 |
36 | constructor(private viewContainer: ViewContainerRef,
37 | private templateRef: TemplateRef,
38 | private tokenService: AngularTokenService) {
39 | }
40 |
41 | ngOnInit(): void {
42 | if (this.role === this.tokenService.currentUserType) {
43 | this.viewContainer.createEmbeddedView(this.templateRef);
44 | } else {
45 | this.viewContainer.clear();
46 | }
47 | }
48 | }
49 | ```
50 |
51 | #### Example:
52 | ```html
53 | Public link 1
54 | Public link 2
55 | Private link 1
56 | Private link 2
57 | ```
--------------------------------------------------------------------------------
/projects/angular-token/src/lib/angular-token.interceptor.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { HttpEvent, HttpRequest, HttpInterceptor, HttpHandler, HttpResponse, HttpErrorResponse } from '@angular/common/http';
3 |
4 | import { AngularTokenService } from './angular-token.service';
5 |
6 | import { Observable } from 'rxjs';
7 | import { tap } from 'rxjs/operators';
8 |
9 | @Injectable()
10 | export class AngularTokenInterceptor implements HttpInterceptor {
11 |
12 | constructor( private tokenService: AngularTokenService ) { }
13 |
14 | intercept(req: HttpRequest, next: HttpHandler): Observable> {
15 |
16 | // Get auth data from local storage
17 | this.tokenService.getAuthDataFromStorage();
18 |
19 | // Add the headers if the request is going to the configured server
20 | const authData = this.tokenService.authData.value;
21 |
22 | if (authData &&
23 | (this.tokenService.tokenOptions.apiBase === null || req.url.match(this.tokenService.tokenOptions.apiBase))) {
24 |
25 | const headers = {
26 | 'access-token': authData.accessToken,
27 | 'client': authData.client,
28 | 'expiry': authData.expiry,
29 | 'token-type': authData.tokenType,
30 | 'uid': authData.uid
31 | };
32 |
33 | req = req.clone({
34 | setHeaders: headers
35 | });
36 | }
37 |
38 | return next.handle(req).pipe(tap(
39 | res => this.handleResponse(res),
40 | err => this.handleResponse(err)
41 | ));
42 | }
43 |
44 |
45 | // Parse Auth data from response
46 | private handleResponse(res: HttpResponse | HttpErrorResponse | HttpEvent): void {
47 | if (res instanceof HttpResponse || res instanceof HttpErrorResponse) {
48 | if (this.tokenService.tokenOptions.apiBase === null || (res.url && res.url.match(this.tokenService.tokenOptions.apiBase))) {
49 | this.tokenService.getAuthHeadersFromResponse(res);
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including
12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * By default, zone.js will patch all possible macroTask and DomEvents
23 | * user can disable parts of macroTask/DomEvents patch by setting following flags
24 | * because those flags need to be set before `zone.js` being loaded, and webpack
25 | * will put import in the top of bundle, so user need to create a separate file
26 | * in this directory (for example: zone-flags.ts), and put the following flags
27 | * into that file, and then add the following code before importing zone.js.
28 | * import './zone-flags';
29 | *
30 | * The flags allowed in zone-flags.ts are listed here.
31 | *
32 | * The following flags will work for all browsers.
33 | *
34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 | *
38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
40 | *
41 | * (window as any).__Zone_enable_cross_context_check = true;
42 | *
43 | */
44 |
45 | /***************************************************************************************************
46 | * Zone JS is required by default for Angular itself.
47 | */
48 | import 'zone.js'; // Included with Angular CLI.
49 |
50 |
51 | /***************************************************************************************************
52 | * APPLICATION IMPORTS
53 | */
54 |
--------------------------------------------------------------------------------
/projects/angular-token/src/lib/angular-token.model.ts:
--------------------------------------------------------------------------------
1 | // Function Data
2 | import { Provider } from '@angular/core';
3 |
4 | export interface SignInData {
5 | login: string;
6 | password: string;
7 | userType?: string;
8 | }
9 |
10 | export interface RegisterData {
11 | login: string;
12 | password: string;
13 | passwordConfirmation: string;
14 | name?: string;
15 | userType?: string;
16 | }
17 |
18 | export interface RegisterData {
19 | [key: string]: string | undefined;
20 | }
21 |
22 | export interface UpdatePasswordData {
23 | password: string;
24 | passwordConfirmation: string;
25 | passwordCurrent?: string;
26 | userType?: string;
27 | resetPasswordToken?: string;
28 | }
29 |
30 | export interface ResetPasswordData {
31 | login: string;
32 | userType?: string;
33 | additionalData?: any;
34 | }
35 |
36 | // API Response Format
37 |
38 | export interface ApiResponse {
39 | status?: string;
40 | success?: boolean;
41 | statusText?: string;
42 | data?: UserData;
43 | errors?: any;
44 | }
45 |
46 | // State Data
47 |
48 | export interface AuthData {
49 | accessToken: string;
50 | client: string;
51 | expiry: string;
52 | tokenType: string;
53 | uid: string;
54 | }
55 |
56 | export interface UserData {
57 | id: number;
58 | provider: string;
59 | uid: string;
60 | name: string;
61 | email: string;
62 | nickname: string;
63 | image: any;
64 | login: string;
65 | }
66 |
67 | // Configuration Options
68 |
69 | export interface UserType {
70 | name: string;
71 | path: string;
72 | }
73 |
74 | export interface TokenInAppBrowser {
75 | create(url: string, target?: string, options?: string | Y): T;
76 | }
77 |
78 | export interface TokenPlatform {
79 | is(platformName: string): boolean;
80 | }
81 |
82 | export interface AngularTokenOptions {
83 | angularTokenOptionsProvider?: Provider;
84 |
85 | apiBase?: string;
86 | apiPath?: string;
87 |
88 | signInPath?: string;
89 | signInRedirect?: string;
90 | signInStoredUrlStorageKey?: string;
91 |
92 | signOutPath?: string;
93 | validateTokenPath?: string;
94 | signOutFailedValidate?: boolean;
95 |
96 | deleteAccountPath?: string;
97 | registerAccountPath?: string;
98 | registerAccountCallback?: string;
99 |
100 | updatePasswordPath?: string;
101 |
102 | resetPasswordPath?: string;
103 | resetPasswordCallback?: string;
104 |
105 | userTypes?: UserType[];
106 | loginField?: string;
107 |
108 | oAuthBase?: string;
109 | oAuthPaths?: { [key: string]: string; };
110 | oAuthCallbackPath?: string;
111 | oAuthWindowType?: string;
112 | oAuthWindowOptions?: { [key: string]: string; };
113 | oAuthBrowserCallbacks?: { [key: string]: string; };
114 | }
115 |
--------------------------------------------------------------------------------
/docs/migrate-to-7.md:
--------------------------------------------------------------------------------
1 | The library has been renamed to **Angular-Token** and now includes Http Interceptor support. Please follow the steps below to upgrade from older versions.
2 |
3 | ### 1. Install new NPM package
4 | The name of the npm has been changed as well. Please uninstall the older package and install the new one.
5 | ```bash
6 | npm uninstall angular2-token
7 |
8 | npm install angular-token
9 | ```
10 |
11 | ### 2. Change imports
12 | Change all imports form 'angular2-token' to 'angular-token'. The module, service and models imported have been renamed as well. Removing the '2' in each name should do the trick.
13 |
14 | #### Before:
15 | ```js
16 | import {
17 | Angular2TokenService,
18 | Angular2TokenModule,
19 | Angular2TokenOptions
20 | ...
21 | } from 'angular2-token';
22 | ```
23 |
24 | #### After:
25 | ```js
26 | import {
27 | AngularTokenService,
28 | AngularTokenModule,
29 | AngularTokenOptions
30 | ...
31 | } from 'angular-token';
32 | ```
33 |
34 | ### 3. Change from Service import to Module import
35 | Import `AngularTokenModule` in your root module instead of providing `Angular2TokenService`. Also remove the `.init()` and provide the `Angular2TokenOptions` via the `.forRoot()` function.
36 |
37 | #### Before:
38 | ```js
39 | // Root Module
40 | @NgModule({
41 | imports: [ ... ],
42 | declarations: [ ... ],
43 | providers: [ Angular2TokenService ],
44 | bootstrap: [ ... ]
45 | })
46 |
47 | // Root Component
48 | constructor(private tokenService: Angular2TokenService) {
49 | this.tokenService.init();
50 | }
51 | ```
52 |
53 | #### After:
54 | ```js
55 | @NgModule({
56 | imports: [
57 | ...,
58 | HttpClientModule,
59 | AngularTokenModule.forRoot({
60 | ...
61 | })
62 | ],
63 | declarations: [ ... ],
64 | providers: [ AngularTokenModule ],
65 | bootstrap: [ ... ]
66 | })
67 | ```
68 |
69 | ### 4. Update '.signIn()', '.register()' and '.resetPassword()' to use 'login' rather than 'email'.
70 | To allow for more flexibility in the login, we changed the models to accept 'login' rather than 'email'.
71 |
72 | #### Before:
73 | ```js
74 | constructor(private tokenService: Angular2TokenService) { }
75 |
76 | this.tokenService.signIn({
77 | email: 'example@example.org',
78 | password: 'secretPassword'
79 | }).subscribe(
80 | res => console.log(res),
81 | error => console.log(error)
82 | );
83 | ```
84 |
85 | #### After:
86 | ```js
87 | this.tokenService.signIn({
88 | login: 'example@example.org',
89 | password: 'secretPassword'
90 | }).subscribe(
91 | res => console.log(res),
92 | error => console.log(error)
93 | );
94 | ```
95 |
96 | ### 5. Use regular HttpClient calls
97 | Replace all tokenService wrapper calls (get, post, put, delete, etc) with regular HttpClient calls.
98 |
99 | #### Before:
100 | ```js
101 | constructor(private tokenService: Angular2TokenService) { }
102 |
103 | this.tokenService.get('my-resource/1').subscribe(
104 | res => console.log(res),
105 | error => console.log(error)
106 | );
107 | ```
108 |
109 | #### After:
110 | ```js
111 | constructor(private http: HttpClient) { }
112 |
113 | this.http.get('my-resource/1').subscribe(
114 | res => console.log(res),
115 | error => console.log(error)
116 | );
117 | ```
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-token-app": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "prefix": "app",
11 | "schematics": {
12 | "@schematics/angular:component": {
13 | "style": "scss"
14 | }
15 | },
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:browser",
19 | "options": {
20 | "outputPath": "dist/angular-token-app",
21 | "index": "src/index.html",
22 | "main": "src/main.ts",
23 | "polyfills": "src/polyfills.ts",
24 | "tsConfig": "tsconfig.app.json",
25 | "inlineStyleLanguage": "scss",
26 | "assets": [
27 | "src/favicon.ico",
28 | "src/assets"
29 | ],
30 | "styles": [
31 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
32 | "src/styles.scss"
33 | ],
34 | "scripts": []
35 | },
36 | "configurations": {
37 | "production": {
38 | "budgets": [
39 | {
40 | "type": "initial",
41 | "maximumWarning": "500kb",
42 | "maximumError": "1mb"
43 | },
44 | {
45 | "type": "anyComponentStyle",
46 | "maximumWarning": "2kb",
47 | "maximumError": "4kb"
48 | }
49 | ],
50 | "fileReplacements": [
51 | {
52 | "replace": "src/environments/environment.ts",
53 | "with": "src/environments/environment.prod.ts"
54 | }
55 | ],
56 | "outputHashing": "all"
57 | },
58 | "development": {
59 | "buildOptimizer": false,
60 | "optimization": false,
61 | "vendorChunk": true,
62 | "extractLicenses": false,
63 | "sourceMap": true,
64 | "namedChunks": true
65 | }
66 | },
67 | "defaultConfiguration": "production"
68 | },
69 | "serve": {
70 | "builder": "@angular-devkit/build-angular:dev-server",
71 | "configurations": {
72 | "production": {
73 | "browserTarget": "angular-token-app:build:production"
74 | },
75 | "development": {
76 | "browserTarget": "angular-token-app:build:development"
77 | }
78 | },
79 | "defaultConfiguration": "development"
80 | }
81 | }
82 | },
83 | "angular-token": {
84 | "projectType": "library",
85 | "root": "projects/angular-token",
86 | "sourceRoot": "projects/angular-token/src",
87 | "prefix": "lib",
88 | "architect": {
89 | "build": {
90 | "builder": "@angular-devkit/build-angular:ng-packagr",
91 | "options": {
92 | "project": "projects/angular-token/ng-package.json"
93 | },
94 | "configurations": {
95 | "production": {
96 | "tsConfig": "projects/angular-token/tsconfig.lib.prod.json"
97 | },
98 | "development": {
99 | "tsConfig": "projects/angular-token/tsconfig.lib.json"
100 | }
101 | },
102 | "defaultConfiguration": "production"
103 | },
104 | "test": {
105 | "builder": "@angular-devkit/build-angular:karma",
106 | "options": {
107 | "main": "projects/angular-token/src/test.ts",
108 | "tsConfig": "projects/angular-token/tsconfig.spec.json",
109 | "karmaConfig": "projects/angular-token/karma.conf.js"
110 | }
111 | }
112 | }
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://badge.fury.io/js/angular-token)
4 | [](https://npmjs.org/angular-token)
5 | [](https://travis-ci.org/neroniaky/angular-token)
6 | [](https://angular.io/styleguide)
7 |
8 | 🔑 Token based authentication service for Angular with interceptor and multi-user support. Works best with the [devise token auth](https://github.com/lynndylanhurley/devise_token_auth) gem for Rails.
9 |
10 | 👋 This library has been renamed to **Angular-Token**! Please follow the [migration guide](https://angular-token.gitbook.io/docs/migrate-to-7).
11 |
12 | ---
13 |
14 | ### Quick Links
15 |
16 | - 🚀 View to demo on [Stackblitz](https://stackblitz.com/github/neroniaky/angular-token)
17 | - ✨ Learn about it on the [docs site](https://angular-token.gitbook.io/docs)
18 | - 🔧 Support us by [contributing](https://angular-token.gitbook.io/docs/contribute)
19 |
20 | ---
21 |
22 | ## Install
23 | 0. Set up a Rails with [Devise Token Auth](https://github.com/lynndylanhurley/devise_token_auth)
24 |
25 | 1. Install Angular-Token via NPM with
26 | ```bash
27 | npm install angular-token
28 | ```
29 |
30 | 2. Import and add `AngularTokenModule` to your main module and call the 'forRoot' function with the config. Make sure you have `HttpClientModule` imported too.
31 | ```javascript
32 | import { AngularTokenModule } from 'angular-token';
33 |
34 | @NgModule({
35 | imports: [
36 | ...,
37 | HttpClientModule,
38 | AngularTokenModule.forRoot({
39 | ...
40 | })
41 | ],
42 | declarations: [ ... ],
43 | bootstrap: [ ... ]
44 | })
45 | ```
46 |
47 | 3. (Maybe Optional) Fix injection context runtime error
48 | After installing this package, if you get an `Error: inject() must be called from an injection context` when running your app, add the following to your typescript path config in the `tsconfig[.app].json` file:
49 | ```json
50 | "paths": {
51 | "@angular/*": [ "./node_modules/@angular/*" ]
52 | }
53 | ```
54 |
55 | ## Use
56 |
57 | 1. Register your user
58 | ```javascript
59 | constructor(private tokenService: AngularTokenService) { }
60 |
61 | this.tokenService.registerAccount({
62 | login: 'example@example.org',
63 | password: 'secretPassword',
64 | passwordConfirmation: 'secretPassword'
65 | }).subscribe(
66 | res => console.log(res),
67 | error => console.log(error)
68 | );
69 | ```
70 |
71 | 2. Sign in your user
72 | ```javascript
73 | constructor(private tokenService: AngularTokenService) { }
74 |
75 | this.tokenService.signIn({
76 | login: 'example@example.org',
77 | password: 'secretPassword'
78 | }).subscribe(
79 | res => console.log(res),
80 | error => console.log(error)
81 | );
82 | ```
83 |
84 | 3. Now you can use HttpClient to access private resources
85 | ```javascript
86 | constructor(http: HttpClient) { }
87 |
88 | this.http.get('private_resource').subscribe(
89 | res => console.log(res),
90 | error => console.log(error)
91 | );
92 | ```
93 |
94 | ## Contributors
95 |
96 | | [ Jan-Philipp Riethmacher](https://github.com/neroniaky) | [ Arjen Brandenburgh](https://github.com/arjenbrandenburgh)
97 | | :---: | :---: |
98 |
99 | ### License
100 | The MIT License (see the [LICENSE](https://github.com/neroniaky/angular-token/blob/master/LICENSE) file for the full text)
101 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | Configuration options can be passed as `AngularTokenOptions` via `forRoot()`.
2 |
3 | ### Default Configuration
4 | ```javascript
5 | @NgModule({
6 | imports: [
7 | AngularTokenModule.forRoot({
8 | apiBase: null,
9 | apiPath: null,
10 |
11 | signInPath: 'auth/sign_in',
12 | signInRedirect: null,
13 | signInStoredUrlStorageKey: null,
14 |
15 | signOutPath: 'auth/sign_out',
16 | validateTokenPath: 'auth/validate_token',
17 | signOutFailedValidate: false,
18 |
19 | registerAccountPath: 'auth',
20 | deleteAccountPath: 'auth',
21 | registerAccountCallback: window.location.href,
22 |
23 | updatePasswordPath: 'auth',
24 | resetPasswordPath: 'auth/password',
25 | resetPasswordCallback: window.location.href,
26 |
27 | oAuthBase: window.location.origin,
28 | oAuthPaths: {
29 | github: 'auth/github'
30 | },
31 | oAuthCallbackPath: 'oauth_callback',
32 | oAuthWindowType: 'newWindow',
33 | oAuthWindowOptions: null,
34 |
35 | userTypes?: null,
36 | loginField?: string;
37 | });
38 | }
39 | }
40 | ```
41 | ### Path options
42 | | Option | Description |
43 | | --------------------------------------- | ---------------------------------------- |
44 | | `apiBase?: string` | Sets the server for all API calls. |
45 | | `apiPath?: string` | Sets base path all operations are based on |
46 | | `signInPath?: string` | Sets path for sign in |
47 | | `signInRedirect?: string` | Sets redirect path for failed CanActivate |
48 | | `signOutPath?: string` | Sets path for sign out |
49 | | `validateTokenPath?: string` | Sets path for token validation |
50 | | `registerAccountPath?: string` | Sets path for account registration |
51 | | `deleteAccountPath?: string` | Sets path for account deletion |
52 | | `updatePasswordPath?: string` | Sets path for password update |
53 | | `resetPasswordPath?: string` | Sets path for password reset |
54 | | `registerAccountCallback?: string` | Sets the path user are redirected to after email confirmation for registration |
55 | | `resetPasswordCallback?: string` | Sets the path user are redirected to after email confirmation for password reset |
56 |
57 | ### Library behaviour options
58 | | Option | Description |
59 | | --------------------------------------- | ---------------------------------------- |
60 | | `signInStoredUrlStorageKey?: string` | Sets locale storage key to store URL before displaying signIn page |
61 | | `signOutFailedValidate?: boolean` | Signs user out when validation returns a 401 status |
62 | | `userTypes?: UserTypes[]` | Allows the configuration of multiple user types (see [Multiple User Types](#multiple-user-types)) |
63 | | `loginField?: string` | Allows the ability to configure a custom login field. Defaults to 'email' |
64 |
65 | ### OAuth options
66 | | Options | Description |
67 | | --------------------------------------- | ----------------------------------------------- |
68 | | `oAuthPaths?: { [key:string]: string }` | Sets paths for sign in with OAuth |
69 | | `oAuthCallbackPath?: string` | Sets path for OAuth sameWindow callback |
70 | | `oAuthBase?: string` | Configure the OAuth server (used for backends on a different url) |
71 | | `oAuthWindowType?:`string` | Window type for Oauth authentication |
72 | | `oAuthWindowOptions?: { [key:string]: string }` | Set additional options to pass into `window.open()` |
73 |
74 | Further information on paths/routes can be found at
75 | [devise token auth](https://github.com/lynndylanhurley/devise_token_auth#usage-tldr).
76 |
--------------------------------------------------------------------------------
/projects/angular-token/src/lib/angular-token.interceptor.spec.ts:
--------------------------------------------------------------------------------
1 | import { HttpClientModule, HttpClient } from '@angular/common/http';
2 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
3 | import { TestBed, inject } from '@angular/core/testing';
4 |
5 | import { AngularTokenModule } from './angular-token.module';
6 | import { AngularTokenService } from './angular-token.service';
7 | import { AngularTokenOptions } from './angular-token.model';
8 |
9 | describe('AngularTokenInterceptor', () => {
10 |
11 | // Init common test data
12 | const tokenType = 'Bearer';
13 | const uid = 'test@test.com';
14 | const accessToken = 'fJypB1ugmWHJfW6CELNfug';
15 | const client = '5dayGs4hWTi4eKwSifu_mg';
16 | const expiry = '1472108318';
17 |
18 | // let service: AngularTokenService;
19 | let backend: HttpTestingController;
20 |
21 | function initService(serviceConfig: AngularTokenOptions) {
22 | // Inject HTTP and AngularTokenService
23 | TestBed.configureTestingModule({
24 | imports: [
25 | HttpClientModule,
26 | HttpClientTestingModule,
27 | AngularTokenModule.forRoot(serviceConfig)
28 | ],
29 | providers: [
30 | AngularTokenService
31 | ]
32 | });
33 |
34 | backend = TestBed.inject(HttpTestingController);
35 | }
36 |
37 | beforeEach(() => {
38 | // Fake Local Storage
39 | let store: { [key: string]: string; } = {};
40 |
41 | const fakeSessionStorage = {
42 | setItem: (key: string, value: string) => store[key] = `${value}`,
43 | getItem: (key: string): string => key in store ? store[key] : null,
44 | removeItem: (key: string) => delete store[key],
45 | clear: () => store = {}
46 | };
47 |
48 | spyOn(Storage.prototype, 'setItem').and.callFake(fakeSessionStorage.setItem);
49 | spyOn(Storage.prototype, 'getItem').and.callFake(fakeSessionStorage.getItem);
50 | spyOn(Storage.prototype, 'removeItem').and.callFake(fakeSessionStorage.removeItem);
51 | spyOn(Storage.prototype, 'clear').and.callFake(fakeSessionStorage.clear);
52 | });
53 |
54 | afterEach(() => {
55 | backend.verify();
56 | });
57 |
58 | /**
59 | *
60 | * Test Http Interceptor
61 | *
62 | */
63 |
64 | describe('http interceptor', () => {
65 |
66 | describe('with apiBase', () => {
67 | beforeEach(() => {
68 | localStorage.setItem('accessToken', accessToken);
69 | localStorage.setItem('client', client);
70 | localStorage.setItem('expiry', expiry);
71 | localStorage.setItem('tokenType', tokenType);
72 | localStorage.setItem('uid', uid);
73 |
74 | initService({
75 | apiBase: 'http://localhost'
76 | });
77 | });
78 |
79 | it('should add authorization headers when to same domain', inject([HttpClient], (http: HttpClient) => {
80 | const testUrl = 'http://localhost/random-endpoint';
81 |
82 | http.get(testUrl).subscribe(response => expect(response).toBeTruthy());
83 |
84 | const req = backend.expectOne({
85 | url: testUrl,
86 | method: 'GET'
87 | });
88 | req.flush({data: 'test'});
89 |
90 | expect(req.request.headers.get('access-token')).toBe(accessToken);
91 | expect(req.request.headers.get('client')).toBe(client);
92 | expect(req.request.headers.get('expiry')).toBe(expiry);
93 | expect(req.request.headers.get('token-type')).toBe(tokenType);
94 | expect(req.request.headers.get('uid')).toBe(uid);
95 | }));
96 |
97 | it('should not add authorization headers when to different domain', inject([HttpClient], (http: HttpClient) => {
98 | const testUrl = 'http://not-local-host/random-endpoint';
99 |
100 | http.get(testUrl).subscribe(response => expect(response).toBeTruthy());
101 |
102 | const req = backend.expectOne({
103 | url: testUrl,
104 | method: 'GET'
105 | });
106 | req.flush({data: 'test'});
107 |
108 | expect(req.request.headers.get('access-token')).toBeNull();
109 | expect(req.request.headers.get('client')).toBeNull();
110 | expect(req.request.headers.get('expiry')).toBeNull();
111 | expect(req.request.headers.get('token-type')).toBeNull();
112 | expect(req.request.headers.get('uid')).toBeNull();
113 | }));
114 |
115 | });
116 |
117 | describe('without apiBase', () => {
118 | beforeEach(() => {
119 | localStorage.setItem('accessToken', accessToken);
120 | localStorage.setItem('client', client);
121 | localStorage.setItem('expiry', expiry);
122 | localStorage.setItem('tokenType', tokenType);
123 | localStorage.setItem('uid', uid);
124 |
125 | initService({});
126 | });
127 |
128 | it('should add authorization headers', inject([HttpClient], (http: HttpClient) => {
129 | const testUrl = 'http://localhost/random-endpoint';
130 |
131 | http.get(testUrl).subscribe(response => expect(response).toBeTruthy());
132 |
133 | const req = backend.expectOne({
134 | url: testUrl,
135 | method: 'GET'
136 | });
137 | req.flush({data: 'test'});
138 |
139 | expect(req.request.headers.get('access-token')).toBe(accessToken);
140 | expect(req.request.headers.get('client')).toBe(client);
141 | expect(req.request.headers.get('expiry')).toBe(expiry);
142 | expect(req.request.headers.get('token-type')).toBe(tokenType);
143 | expect(req.request.headers.get('uid')).toBe(uid);
144 | }));
145 | });
146 |
147 | describe('handleResponse', () => {
148 | beforeEach(() => {
149 | initService({});
150 | });
151 |
152 | it('should handle headers from a request', inject([HttpClient], (http: HttpClient) => {
153 | const testUrl = 'http://localhost/random-endpoint';
154 |
155 | http.get(testUrl).subscribe(response => expect(response).toBeTruthy());
156 |
157 | const req = backend.expectOne({
158 | url: testUrl,
159 | method: 'GET'
160 | });
161 | req.flush({data: 'test'}, {
162 | headers: {
163 | 'access-token': accessToken,
164 | 'client': client,
165 | 'expiry': expiry,
166 | 'token-type': tokenType,
167 | 'uid': uid
168 | }
169 | });
170 |
171 | expect(localStorage.getItem('accessToken')).toBe(accessToken);
172 | expect(localStorage.getItem('client')).toBe(client);
173 | expect(localStorage.getItem('expiry')).toBe(expiry);
174 | expect(localStorage.getItem('tokenType')).toBe(tokenType);
175 | expect(localStorage.getItem('uid')).toBe(uid);
176 | }));
177 | });
178 | });
179 | });
180 |
--------------------------------------------------------------------------------
/docs/session-management.md:
--------------------------------------------------------------------------------
1 | Once initialized `AngularTokenService` offers methods for session management:
2 |
3 | - [`.signIn()`](#signin)
4 | - [`.signOut()`](#signout)
5 | - [`.registerAccount()`](#registeraccount)
6 | - [`.deleteAccount()`](#deleteaccount)
7 | - [`.validateToken()`](#validatetoken)
8 | - [`.updatePassword()`](#updatepassword)
9 | - [`.resetPassword()`](#resetpassword)
10 | - [`.signInOAuth()`](#signinoauth)
11 | - [`.processOAuthCallback()`](#processoauthcallback)
12 |
13 | ## .signIn()
14 | The signIn method is used to sign in the user with login (e.g. email address) and password.
15 | The optional parameter `userType` specifies the name of UserType used for this session.
16 |
17 | The optional parameter `additionalData` allows to pass custom data for the login logic (Example use case: Recaptcha).
18 |
19 | `signIn({login: string, password: string, userType?: string}, additionalData?: any): Observable`
20 |
21 | #### Example:
22 | ```javascript
23 | this.tokenService.signIn({
24 | login: 'example@example.org',
25 | password: 'secretPassword',
26 | }, additionalData).subscribe(
27 | res => console.log(res),
28 | error => console.log(error)
29 | );
30 | ```
31 |
32 | ## .signOut()
33 | The signOut method destroys session and session storage.
34 |
35 | `signOut(): Observable`
36 |
37 | #### Example:
38 | ```javascript
39 | this.tokenService.signOut().subscribe(
40 | res => console.log(res),
41 | error => console.log(error)
42 | );
43 | ```
44 |
45 | ## .registerAccount()
46 | Sends a new user registration request to the Server.
47 |
48 | The optional parameter `additionalData` allows to pass custom data for the registration logic (Example use case: Recaptcha).
49 |
50 | `registerAccount({login: string, password: string, passwordConfirmation: string, userType?: string}, additionalData?: any): Observable`
51 |
52 | #### Example:
53 | ```javascript
54 | this.tokenService.registerAccount({
55 | login: 'example@example.org',
56 | password: 'secretPassword',
57 | passwordConfirmation: 'secretPassword'
58 | }, additionalData).subscribe(
59 | res => console.log(res),
60 | error => console.log(error)
61 | );
62 | ```
63 |
64 | ## .deleteAccount()
65 | Deletes the account for the signed in user.
66 |
67 | `deleteAccount(): Observable`
68 |
69 | #### Example:
70 | ```javascript
71 | this.tokenService.deleteAccount().subscribe(
72 | res => console.log(res),
73 | error => console.log(error)
74 | );
75 | ```
76 |
77 | ## .validateToken()
78 | Validates the current token with the server.
79 |
80 | `validateToken(): Observable`
81 |
82 | #### Example:
83 | ```javascript
84 | this.tokenService.validateToken().subscribe(
85 | res => console.log(res),
86 | error => console.log(error)
87 | );
88 | ```
89 |
90 | ## .updatePassword()
91 | Updates the password for the logged in user. Note that there are two main flows that this is used for -
92 | a user changing their password while they are already logged in and a "forgot password" flow where the user is doing an update via the link in a reset password email.
93 |
94 | For a normal password update, you need to send the new password twice, for confirmation and you may also have to send the current password for extra security. The setting "check_current_password_before_update" in the Devise Token Auth library is used to control if the current password is required or not.
95 |
96 | For the "forgot password" flow where the user is not logged in, review the .resetPassword() documentation below to understand how .updatePassword() fits into that flow. This library's updatePassword() in a "forgot password" flow is known to work with Angular apps using PathLocationStrategy (the default for Angular) but an [issue](https://github.com/lynndylanhurley/devise_token_auth/issues/599) with the Devise Token Auth library currently prevents apps using HashLocationStrategy from working. (See [https://angular.io/api/common/LocationStrategy](https://angular.io/api/common/LocationStrategy) for an explanation of the two strategies). A PR has been created on the Devise Token Auth library to fix this - watch [PR 1341](https://github.com/lynndylanhurley/devise_token_auth/pull/1341) for the current status on that. Only password and new password are required to be sent for a "forgot password" password update request - the optional resetPasswordToken is not required. Note however that the server library Devise Token Auth has a new feature (currently unreleased in the gem) whereby you can add the resetPasswordToken into this request and it will auth using that token and not require the client lib to add any auth headers to the request, as it currently does. See the section [Mobile alternative flow (use reset_password_token)](https://devise-token-auth.gitbook.io/devise-token-auth/usage/reset_password) in the server side library's documentation.
97 |
98 | `updatePassword({password: string, passwordConfirmation: string, passwordCurrent?: string, userType?: string, resetPasswordToken?: string}): Observable`
99 | ` `
100 | #### Example (change password):
101 | ```javascript
102 | this.tokenService.updatePassword({
103 | password: 'newPassword',
104 | passwordConfirmation: 'newPassword',
105 | passwordCurrent: 'oldPassword',
106 | }).subscribe(
107 | res => console.log(res),
108 | error => console.log(error)
109 | );
110 | ```
111 |
112 | #### Example (reset password):
113 | ```javascript
114 | this.tokenService.updatePassword({
115 | password: 'newPassword',
116 | passwordConfirmation: 'newPassword',
117 | }).subscribe(
118 | res => console.log(res),
119 | error => console.log(error)
120 | );
121 | ```
122 |
123 | ## .resetPassword()
124 | Request a password reset from the server (aka "forgot password" flow). This only asks the server to issue an email with a reset password link it. Once that link is clicked, the server will auth against the reset token in the link, and redirect to your configured resetPasswordCallback url, which should be a component that asks for the new password and confirmation. At that point you are effectively logged into the server with a temporary session and this lib will have valid auth headers ready to be added to the final step which is to issue an updatePassword to actually do the change. The addition of auth headers to that updatePassword request is handled automatically by this lib, you don't need to do anything in your component other than issue the update request. The server side library Devise Token Auth has a useful description of the flow at [https://devise-token-auth.gitbook.io/devise-token-auth/usage/reset_password](https://devise-token-auth.gitbook.io/devise-token-auth/usage/reset_password).
125 |
126 | ********
127 | See the .resetPassword() documentation above for a possible issue with the "forgot password" flow for Angular apps using HashLocationStrategy. For apps using the default PathLocationStrategy everything should work correctly, but be aware of one potentional issue. The redirect after clicking the email link goes through the server under PathLocationStrategy. It is important that the server allows the redirect to flow through to the Angular app without adding any auth tokens. For some servers that "catch all unknown" can be done via low level config, to send any unknown routes into index.html (the start of the Angular app). For example this is usually done via the .htaccess file for an Apache server. For a rails back end, the usual approach is at a higher level, by putting in catch-all final route and rendering that to index.html. It is important the controller used to do that render does not inherit from a controller that has the Devise Token Auth's "concerns" added. In general that means do not base the render controller on the ApplicationController but instead go a level up to the ActionController. For example:
128 |
129 | Catch any unmatched routes in routes.rb:
130 |
131 | `match "*path", to: 'errors#angular' , via: :all`
132 |
133 | Then make sure ErrorsController inherits from ActionController (or somewhere that doesn't have the Devise Auth Token "concerns"). Note the public/index.html is server specifc, yours might be in ,e.g., dist/index.html:
134 |
135 | ```ruby
136 | class ErrosController < ActionController
137 | def angular
138 | render file: 'public/index.html', :layout => false
139 | end
140 | end
141 | ```
142 | ********
143 |
144 | `resetPassword({login: string, userType?: string}, additionalData?: any): Observable`
145 |
146 | #### Example:
147 | ```javascript
148 | this.tokenService.resetPassword({
149 | login: 'example@example.org',
150 | }, additionalData).subscribe(
151 | res => console.log(res),
152 | error => console.log(error)
153 | );
154 | ```
155 |
156 | ## .signInOAuth()
157 | Initiates OAuth authentication flow. Currently, it supports two window modes:
158 | `newWindow` (default) and `sameWindow` (settable in config as `oAuthWindowType`).
159 | - When `oAuthWindowType` is set to `newWindow`, `.signInOAuth()` opens a new window and returns an observable.
160 |
161 | - When `oAuthWindowType` is set to `sameWindow`, `.signInOAuth()` returns nothing and redirects user to auth provider.
162 | After successful authentication, it redirects back to `oAuthCallbackPath`. Application router needs to intercept
163 | this route and call `processOAuthCallback()` to fetch `AuthData` from params.
164 |
165 | `signInOAuth(oAuthType: string)`
166 |
167 | #### Example:
168 |
169 | ```javascript
170 | this.tokenService.signInOAuth(
171 | 'github'
172 | ).subscribe(
173 | res => console.log(res),
174 | error => console.log(error)
175 | );
176 | ```
177 |
178 | ## .processOAuthCallback()
179 | Fetches AuthData from params sent via OAuth redirection in `sameWindow` flow.
180 |
181 | `processOAuthCallback()`
182 |
183 | #### Example
184 |
185 | Callback route:
186 | ```javascript
187 | RouterModule.forRoot([
188 | { path: 'oauth_callback', component: OauthCallbackComponent }
189 | ])
190 | ```
191 |
192 | Callback component:
193 | ```javascript
194 | @Component({
195 | template: ''
196 | })
197 | export class OauthCallbackComponent implements OnInit {
198 | constructor(private tokenService: AngularTokenService) {}
199 |
200 | ngOnInit() {
201 | this.tokenService.processOAuthCallback();
202 | }
203 | }
204 | ```
205 |
--------------------------------------------------------------------------------
/src/app/fake-backend.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS, HttpHeaders } from '@angular/common/http';
3 | import { Observable, of } from 'rxjs';
4 | import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';
5 |
6 | @Injectable()
7 | export class FakeBackendInterceptor implements HttpInterceptor {
8 |
9 | users: any[];
10 |
11 | constructor() { }
12 |
13 | intercept(request: HttpRequest, next: HttpHandler): Observable> {
14 |
15 | // array in local storage for registered users
16 | this.users = JSON.parse(localStorage.getItem('users')) || [];
17 |
18 | // wrap in delayed observable to simulate server api call
19 | return of(null).pipe(mergeMap(() => {
20 |
21 | /*
22 | *
23 | * Register
24 | *
25 | */
26 |
27 | if (request.url === 'https://mock-api-server/auth' && request.method === 'POST') {
28 |
29 | // Get new user object from post body
30 | const body = request.body;
31 |
32 | // Check if all inputs provided
33 | if (body.email === null && body.password === null && body.password_confirmation === null) {
34 | return of(this.registerError(
35 | body.email,
36 | 'Please submit proper sign up data in request body.'
37 | ));
38 | }
39 |
40 | // Check if password matches password confimation
41 | if (body.password !== body.password_confirmation) {
42 | return of(this.registerError(
43 | body.email,
44 | { password_confirmation: ['does not match Password'] }
45 | ));
46 | }
47 |
48 | // Check if login is email
49 | const re = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
50 |
51 | if (!re.test(body.email)) {
52 | return of(this.registerError(
53 | body.email,
54 | { email: ['is not an email'] },
55 | ));
56 | }
57 |
58 | // Check if login already exists
59 | const duplicateUser = this.users.filter(user => {
60 | return user.email === body.email;
61 | }).length;
62 |
63 | if (duplicateUser) {
64 | return of(this.registerError(
65 | body.email,
66 | { email: ['has already been taken'] },
67 | ));
68 | }
69 |
70 | const newUser = {
71 | id: this.users.length + 1,
72 | email: body.email,
73 | password: body.password
74 | };
75 |
76 | this.users.push(newUser);
77 | localStorage.setItem('users', JSON.stringify(this.users));
78 |
79 | // respond 200 OK
80 | return of(new HttpResponse({
81 | status: 200,
82 | url: 'https://mock-api-server/auth',
83 | body: {
84 | status: 'success',
85 | data: {
86 | uid: body.email,
87 | id: this.users.length + 1,
88 | email: body.email,
89 | provider: 'email',
90 | name: null,
91 | nickname: null,
92 | image: null,
93 | created_at: new Date().toISOString(),
94 | updated_at: new Date().toISOString()
95 | }
96 | }
97 | }));
98 | }
99 |
100 | /*
101 | *
102 | * Sign In
103 | *
104 | */
105 |
106 | if (request.url.match('https://mock-api-server/auth/sign_in') && request.method === 'POST') {
107 |
108 | const filteredUsers = this.users.filter(user => {
109 | return user.email === request.body.email && user.password === request.body.password;
110 | });
111 |
112 | if (filteredUsers.length) {
113 | return of(new HttpResponse({
114 | headers: this.getHeaders(filteredUsers[0].email),
115 | status: 200,
116 | url: 'https://mock-api-server/auth/sign_in',
117 | body: {
118 | data: {
119 | id: filteredUsers[0].id,
120 | email: filteredUsers[0].email,
121 | provider: 'email',
122 | uid: filteredUsers[0].email,
123 | name: null,
124 | nickname: null,
125 | image: null
126 | }
127 | }
128 | }));
129 | } else {
130 | // else return 400 bad request
131 | return of(new HttpResponse({
132 | status: 401,
133 | url: 'https://mock-api-server/auth/sign_in',
134 | body: {
135 | status: 'false',
136 | errors: ['Invalid login credentials. Please try again.']
137 | }
138 | }));
139 | }
140 | }
141 |
142 | /*
143 | *
144 | * Sign Out
145 | *
146 | */
147 |
148 | if (request.url.match('https://mock-api-server/auth/sign_out') && request.method === 'DELETE') {
149 | if (request.headers.get('access-token') === 'fake-access-token') {
150 | return of(new HttpResponse({
151 | status: 200,
152 | url: 'https://mock-api-server/auth/sign_out',
153 | body: {
154 | success: true
155 | }
156 | }));
157 | } else {
158 | return of(new HttpResponse({
159 | status: 404,
160 | url: 'https://mock-api-server/auth/sign_out',
161 | body: {
162 | status: 'false',
163 | errors: ['User was not found or was not logged in.']
164 | }
165 | }));
166 | }
167 | }
168 |
169 | /*
170 | *
171 | * Validate Token
172 | *
173 | */
174 |
175 | if (request.url.match('https://mock-api-server/auth/validate_token') && request.method === 'GET') {
176 |
177 | const user = this.getAuthUser(request);
178 |
179 | if (user) {
180 | return of(new HttpResponse({
181 | headers: this.getHeaders(user.email),
182 | status: 200,
183 | url: 'https://mock-api-server/auth/validate_token',
184 | body: {
185 | success: true,
186 | data: {
187 | id: user.id,
188 | provider: 'email',
189 | uid: user.email,
190 | name: null,
191 | nickname: null,
192 | image: null,
193 | email: user.email
194 | }
195 | }
196 | }));
197 | } else {
198 | return of(new HttpResponse({
199 | status: 401,
200 | url: 'https://mock-api-server/auth/validate_token',
201 | body: {
202 | success: false,
203 | errors: ['Invalid login credentials']
204 | }
205 | }));
206 | }
207 | }
208 |
209 | /*
210 | *
211 | * Update Password
212 | *
213 | */
214 |
215 | if (request.url.match('https://mock-api-server/auth') && request.method === 'PUT') {
216 |
217 | // Check if password matches password confimation
218 | if (request.body.password !== request.body.password_confirmation) {
219 | return of(this.registerError(
220 | request.body.email,
221 | { password_confirmation: ['does not match Password'] }
222 | ));
223 | }
224 |
225 | const user = this.getAuthUser(request);
226 |
227 | if (user && user.password === request.body.password) {
228 |
229 | this.users[(user.id - 1)].password = request.body.password;
230 |
231 | localStorage.setItem('users', JSON.stringify(this.users));
232 |
233 | return of(new HttpResponse({
234 | headers: this.getHeaders(user.email),
235 | status: 200,
236 | url: 'https://mock-api-server/auth',
237 | body: {
238 | status: 'success',
239 | data: {
240 | id: user.id,
241 | email: user.email,
242 | uid: user.email,
243 | provider: 'email',
244 | name: null,
245 | nickname: null,
246 | image: null,
247 | created_at: new Date().toISOString(),
248 | updated_at: new Date().toISOString()
249 | }
250 | }
251 | }));
252 | } else {
253 | return of(new HttpResponse({
254 | status: 401,
255 | url: 'https://mock-api-server/auth',
256 | body: {
257 | success: false,
258 | errors: ['Invalid login credentials']
259 | }
260 | }));
261 | }
262 | }
263 |
264 | /*
265 | *
266 | * Access Private Resouce
267 | *
268 | */
269 |
270 | if (request.url.match('https://mock-api-server/private_resource') && request.method === 'GET') {
271 |
272 | const user = this.getAuthUser(request);
273 |
274 | if (user) {
275 | return of(new HttpResponse({
276 | headers: this.getHeaders(user.email),
277 | status: 200,
278 | url: 'https://mock-api-server/auth/private_resource',
279 | body: {
280 | data: 'Private Content for ' + user.email
281 | }
282 | }));
283 | } else {
284 | return of(new HttpResponse({
285 | status: 401,
286 | url: 'https://mock-api-server/auth/private_resource',
287 | body: {
288 | success: false,
289 | errors: ['Invalid login credentials']
290 | }
291 | }));
292 | }
293 | }
294 |
295 | // pass through any requests not handled above
296 | return next.handle(request);
297 | }))
298 |
299 | // call materialize and dematerialize to ensure delay even if an
300 | // error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648)
301 | .pipe(materialize())
302 | .pipe(delay(500))
303 | .pipe(dematerialize());
304 | }
305 |
306 | getAuthUser(request: HttpRequest) {
307 | const filteredUsers = this.users.filter(user => user.email === request.headers.get('uid'));
308 |
309 | if (filteredUsers.length && request.headers.get('access-token') === 'fake-access-token') {
310 | return filteredUsers[0];
311 | } else {
312 | return undefined;
313 | }
314 | }
315 |
316 | getHeaders(uid: string): HttpHeaders {
317 | const timestamp = String(Math.floor(Date.now() / 1000) + 600);
318 |
319 | // if login details are valid return 200 OK with user details and fake jwt token
320 | return new HttpHeaders({
321 | 'access-token': 'fake-access-token',
322 | 'client': 'fake-client-id',
323 | 'expiry': timestamp,
324 | 'token-type': 'Bearer',
325 | 'uid': uid
326 | });
327 | }
328 |
329 | registerError(email: string, errorMsg?: {[key: string]: string[]} | string) {
330 | return new HttpResponse({
331 | status: 422, url: 'https://mock-api-server/auth', body: {
332 | status: 'error',
333 | data: {
334 | id: null,
335 | provider: 'email',
336 | uid: '',
337 | name: null,
338 | nickname: null,
339 | image: null,
340 | email: email,
341 | created_at: null,
342 | updated_at: null
343 | },
344 | errors: errorMsg
345 | }
346 | });
347 | }
348 | }
349 |
350 | export let fakeBackendProvider = {
351 | // use fake backend in place of Http service for backend-less development
352 | provide: HTTP_INTERCEPTORS,
353 | useClass: FakeBackendInterceptor,
354 | multi: true
355 | };
356 |
--------------------------------------------------------------------------------
/projects/angular-token/src/lib/angular-token.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { HttpClientModule } from '@angular/common/http';
2 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
3 | import { TestBed } from '@angular/core/testing';
4 |
5 | import { AngularTokenModule } from './angular-token.module';
6 | import { AngularTokenService } from './angular-token.service';
7 | import {
8 | SignInData,
9 | RegisterData,
10 | UpdatePasswordData,
11 | ResetPasswordData,
12 | AuthData,
13 | UserData,
14 | AngularTokenOptions
15 | } from './angular-token.model';
16 |
17 | describe('AngularTokenService', () => {
18 |
19 | // Init common test data
20 | const tokenType = 'Bearer';
21 | const uid = 'test@test.com';
22 | const accessToken = 'fJypB1ugmWHJfW6CELNfug';
23 | const client = '5dayGs4hWTi4eKwSifu_mg';
24 | const expiry = '1472108318';
25 |
26 | const tokenHeaders = {
27 | 'content-Type': 'application/json',
28 | 'token-type': tokenType,
29 | 'uid': uid,
30 | 'access-token': accessToken,
31 | 'client': client,
32 | 'expiry': expiry
33 | };
34 |
35 | const authData: AuthData = {
36 | tokenType: tokenType,
37 | uid: uid,
38 | accessToken: accessToken,
39 | client: client,
40 | expiry: expiry
41 | };
42 |
43 | const userData: UserData = {
44 | id: 1,
45 | provider: 'provider',
46 | uid: 'uid',
47 | name: 'name',
48 | email: 'email',
49 | nickname: 'nickname',
50 | image: null,
51 | login: 'test@example.com'
52 | };
53 |
54 | // SignIn test data
55 | const signInData: SignInData = {
56 | login: 'test@example.com',
57 | password: 'password'
58 | };
59 |
60 | const signInDataOutput = {
61 | email: 'test@example.com',
62 | password: 'password'
63 | };
64 |
65 | const signInDataCustomOutput = {
66 | username: 'test@example.com',
67 | password: 'password'
68 | };
69 |
70 | // Register test data
71 | const registerData: RegisterData = {
72 | login: 'test@example.com',
73 | password: 'password',
74 | passwordConfirmation: 'password'
75 | };
76 |
77 | // Register test data
78 | const registerCustomFieldsData: RegisterData = {
79 | login: 'test@example.com',
80 | first_name: 'John',
81 | last_name: 'Doe',
82 | password: 'password',
83 | passwordConfirmation: 'password'
84 | };
85 |
86 | const registerCustomFieldsDataOutput = {
87 | email: 'test@example.com',
88 | first_name: 'John',
89 | last_name: 'Doe',
90 | password: 'password',
91 | password_confirmation: 'password',
92 | confirm_success_url: window.location.href
93 | };
94 |
95 | const registerDataOutput = {
96 | email: 'test@example.com',
97 | password: 'password',
98 | password_confirmation: 'password',
99 | confirm_success_url: window.location.href
100 | };
101 |
102 | const registerCustomDataOutput = {
103 | username: 'test@example.com',
104 | password: 'password',
105 | password_confirmation: 'password',
106 | confirm_success_url: window.location.href
107 | };
108 |
109 | // Update password data
110 | const updatePasswordData: UpdatePasswordData = {
111 | password: 'newpassword',
112 | passwordConfirmation: 'newpassword',
113 | passwordCurrent: 'oldpassword'
114 | };
115 |
116 | const updatePasswordDataOutput = {
117 | current_password: 'oldpassword',
118 | password: 'newpassword',
119 | password_confirmation: 'newpassword'
120 | };
121 |
122 | // Reset password data
123 | const resetPasswordData: ResetPasswordData = {
124 | login: 'test@example.com',
125 | };
126 |
127 | const resetPasswordDataOutput = {
128 | email: 'test@example.com',
129 | redirect_url: 'http://localhost:9876/context.html'
130 | };
131 |
132 | const resetCustomPasswordDataOutput = {
133 | username: 'test@example.com',
134 | redirect_url: 'http://localhost:9876/context.html'
135 | };
136 |
137 | let service: AngularTokenService;
138 | let backend: HttpTestingController;
139 | let fakeWindow = { open: (a: string, b: string, c: string): void => null, location: { href: window.location.href, origin: 'http://localhost:9876' } };
140 |
141 | function initService(serviceConfig: AngularTokenOptions) {
142 | // Inject HTTP and AngularTokenService
143 | TestBed.configureTestingModule({
144 | imports: [
145 | HttpClientModule,
146 | HttpClientTestingModule,
147 | AngularTokenModule.forRoot(serviceConfig)
148 | ],
149 | providers: [
150 | AngularTokenService,
151 | { provide: 'Window', useValue: fakeWindow }
152 | ]
153 | });
154 |
155 | service = TestBed.inject(AngularTokenService);
156 | backend = TestBed.inject(HttpTestingController);
157 |
158 | }
159 |
160 | beforeEach(() => {
161 | // Fake Local Storage
162 | let store: { [key: string]: string; } = {};
163 |
164 | const fakeSessionStorage = {
165 | setItem: (key: string, value: string) => store[key] = `${value}`,
166 | getItem: (key: string): string => key in store ? store[key] : null,
167 | removeItem: (key: string) => delete store[key],
168 | clear: () => store = {}
169 | };
170 |
171 | spyOn(Storage.prototype, 'setItem').and.callFake(fakeSessionStorage.setItem);
172 | spyOn(Storage.prototype, 'getItem').and.callFake(fakeSessionStorage.getItem);
173 | spyOn(Storage.prototype, 'removeItem').and.callFake(fakeSessionStorage.removeItem);
174 | spyOn(Storage.prototype, 'clear').and.callFake(fakeSessionStorage.clear);
175 | });
176 |
177 | afterEach(() => {
178 | backend.verify();
179 | fakeWindow = { open: (): void => null, location: { href: window.location.href, origin: 'http://localhost:9876' } };
180 | });
181 |
182 | /**
183 | *
184 | * Test default configuration
185 | *
186 | */
187 |
188 | describe('default configuration', () => {
189 | beforeEach(() => {
190 | initService({});
191 | });
192 |
193 | it('signIn should POST data', () => {
194 |
195 | service.signIn(signInData);
196 |
197 | const req = backend.expectOne({
198 | url: 'auth/sign_in',
199 | method: 'POST'
200 | });
201 |
202 | expect(req.request.body).toEqual(signInDataOutput);
203 | });
204 |
205 | it('signIn method should set local storage', () => {
206 |
207 | service.signIn(signInData).subscribe(data => {
208 | expect(localStorage.getItem('accessToken')).toEqual(accessToken);
209 | expect(localStorage.getItem('client')).toEqual(client);
210 | expect(localStorage.getItem('expiry')).toEqual(expiry);
211 | expect(localStorage.getItem('tokenType')).toEqual(tokenType);
212 | expect(localStorage.getItem('uid')).toEqual(uid);
213 | });
214 |
215 | const req = backend.expectOne({
216 | url: 'auth/sign_in',
217 | method: 'POST'
218 | });
219 |
220 | req.flush(
221 | { login: 'test@email.com' },
222 | { headers: tokenHeaders }
223 | );
224 | });
225 |
226 | it('signOut should DELETE', () => {
227 |
228 | service.signOut().subscribe();
229 |
230 | backend.expectOne({
231 | url: 'auth/sign_out',
232 | method: 'DELETE'
233 | });
234 |
235 | expect(localStorage.getItem('accessToken')).toBeNull();
236 | expect(localStorage.getItem('client')).toBeNull();
237 | expect(localStorage.getItem('expiry')).toBeNull();
238 | expect(localStorage.getItem('tokenType')).toBeNull();
239 | expect(localStorage.getItem('uid')).toBeNull();
240 | });
241 |
242 |
243 | it('signOut should clear local storage', () => {
244 | localStorage.setItem('token-type', tokenType);
245 | localStorage.setItem('uid', uid);
246 | localStorage.setItem('access-token', accessToken);
247 | localStorage.setItem('client', client);
248 | localStorage.setItem('expiry', expiry);
249 |
250 | service.signOut().subscribe(data => {
251 | expect(localStorage.getItem('accessToken')).toBe(null);
252 | expect(localStorage.getItem('client')).toBe(null);
253 | expect(localStorage.getItem('expiry')).toBe(null);
254 | expect(localStorage.getItem('tokenType')).toBe(null);
255 | expect(localStorage.getItem('uid')).toBe(null);
256 | });
257 |
258 | backend.expectOne({
259 | url: 'auth/sign_out',
260 | method: 'DELETE'
261 | });
262 | });
263 |
264 | describe('registerAccount should POST data', () => {
265 | it('with standard fields', () => {
266 |
267 | service.registerAccount(registerData).subscribe();
268 |
269 | const req = backend.expectOne({
270 | url: 'auth',
271 | method: 'POST'
272 | });
273 |
274 | expect(req.request.body).toEqual(registerDataOutput);
275 | });
276 |
277 | it('with custom fields', () => {
278 |
279 | service.registerAccount(registerCustomFieldsData).subscribe();
280 |
281 | const req = backend.expectOne({
282 | url: 'auth',
283 | method: 'POST'
284 | });
285 |
286 | expect(req.request.body).toEqual(registerCustomFieldsDataOutput);
287 | });
288 |
289 | });
290 |
291 | it('validateToken should GET', () => {
292 |
293 | service.validateToken();
294 |
295 | backend.expectOne({
296 | url: 'auth/validate_token',
297 | method: 'GET'
298 | });
299 | });
300 |
301 | it('validateToken should not call signOut when it returns status 401', () => {
302 |
303 | const signOutSpy = spyOn(service, 'signOut');
304 |
305 | service.validateToken().subscribe(() => { }, () => expect(signOutSpy).not.toHaveBeenCalled());
306 |
307 | const req = backend.expectOne({
308 | url: 'auth/validate_token',
309 | method: 'GET'
310 | });
311 |
312 | req.flush('',
313 | {
314 | status: 401,
315 | statusText: 'Not authorized'
316 | }
317 | );
318 | });
319 |
320 | it('updatePassword should PUT', () => {
321 |
322 | service.updatePassword(updatePasswordData).subscribe();
323 |
324 | const req = backend.expectOne({
325 | url: 'auth',
326 | method: 'PUT'
327 | });
328 |
329 | expect(req.request.body).toEqual(updatePasswordDataOutput);
330 | });
331 |
332 | it('resetPassword should POST', () => {
333 |
334 | service.resetPassword(resetPasswordData).subscribe();
335 |
336 | const req = backend.expectOne({
337 | url: 'auth/password',
338 | method: 'POST'
339 | });
340 |
341 | expect(req.request.body).toEqual(resetPasswordDataOutput);
342 | });
343 |
344 | });
345 |
346 | /**
347 | *
348 | * Testing custom configuration
349 | *
350 | */
351 |
352 | describe('custom configuration', () => {
353 | beforeEach(() => {
354 | initService({
355 | apiBase: 'https://localhost',
356 | apiPath: 'myapi',
357 |
358 | signInPath: 'myauth/mysignin',
359 | signOutPath: 'myauth/mysignout',
360 | registerAccountPath: 'myauth/myregister',
361 | deleteAccountPath: 'myauth/mydelete',
362 | validateTokenPath: 'myauth/myvalidate',
363 | updatePasswordPath: 'myauth/myupdate',
364 | resetPasswordPath: 'myauth/myreset',
365 |
366 | loginField: 'username'
367 | });
368 | });
369 |
370 | it('signIn should POST data', () => {
371 |
372 | service.signIn(signInData);
373 |
374 | const req = backend.expectOne({
375 | url: 'https://localhost/myapi/myauth/mysignin',
376 | method: 'POST'
377 | });
378 |
379 | expect(req.request.body).toEqual(signInDataCustomOutput);
380 | });
381 |
382 | it('signOut should DELETE', () => {
383 |
384 | service.signOut().subscribe();
385 |
386 | backend.expectOne({
387 | url: 'https://localhost/myapi/myauth/mysignout',
388 | method: 'DELETE'
389 | });
390 | });
391 |
392 | it('registerAccount should POST data', () => {
393 |
394 | service.registerAccount(registerData).subscribe();
395 |
396 | const req = backend.expectOne({
397 | url: 'https://localhost/myapi/myauth/myregister',
398 | method: 'POST'
399 | });
400 |
401 | expect(req.request.body).toEqual(registerCustomDataOutput);
402 | });
403 |
404 | it('validateToken should GET', () => {
405 |
406 | service.validateToken();
407 |
408 | backend.expectOne({
409 | url: 'https://localhost/myapi/myauth/myvalidate',
410 | method: 'GET'
411 | });
412 | });
413 |
414 | it('updatePassword should PUT', () => {
415 |
416 | service.updatePassword(updatePasswordData).subscribe();
417 |
418 | const req = backend.expectOne({
419 | url: 'https://localhost/myapi/myauth/myupdate',
420 | method: 'PUT'
421 | });
422 |
423 | expect(req.request.body).toEqual(updatePasswordDataOutput);
424 | });
425 |
426 | it('resetPassword should POST', () => {
427 |
428 | service.resetPassword(resetPasswordData).subscribe();
429 |
430 | const req = backend.expectOne({
431 | url: 'https://localhost/myapi/myauth/myreset',
432 | method: 'POST'
433 | });
434 |
435 | expect(req.request.body).toEqual(resetCustomPasswordDataOutput);
436 | });
437 |
438 | });
439 |
440 | describe('signoutValidate', () => {
441 | beforeEach(() => {
442 | initService({
443 | signOutFailedValidate: true
444 | });
445 | });
446 |
447 | it('validateToken should call signOut when it returns status 401', () => {
448 |
449 | const signOutSpy = spyOn(service, 'signOut');
450 |
451 | service.validateToken().subscribe(() => { }, () => expect(signOutSpy).toHaveBeenCalled());
452 |
453 | const req = backend.expectOne({
454 | url: 'auth/validate_token',
455 | method: 'GET'
456 | });
457 |
458 | req.flush('',
459 | {
460 | status: 401,
461 | statusText: 'Not authorized'
462 | }
463 | );
464 |
465 | });
466 | });
467 |
468 | describe('signInOauth', () => {
469 | describe('in a new window', () => {
470 | beforeEach(() => {
471 | initService({
472 | oAuthBase: 'https://www.example.com',
473 | oAuthWindowType: 'newWindow'
474 | })
475 | })
476 |
477 | it('opens a new window with the authentication URL', () => {
478 | const openSpy = spyOn(fakeWindow, 'open');
479 | service.signInOAuth('facebook')
480 | expect(openSpy).toHaveBeenCalledWith(
481 | 'https://www.example.com/auth/facebook?omniauth_window_type=newWindow&auth_origin_url=http%3A%2F%2Flocalhost%3A9876%2Foauth_callback',
482 | '_blank', 'closebuttoncaption=Cancel'
483 | )
484 | })
485 | })
486 |
487 | describe('in the same window', () => {
488 | beforeEach(() => {
489 | initService({
490 | oAuthBase: 'https://www.example.com',
491 | oAuthWindowType: 'sameWindow'
492 | })
493 | })
494 |
495 | it('redirects to the authentication URL', () => {
496 | service.signInOAuth('facebook')
497 | expect(fakeWindow.location.href).toEqual(
498 | 'https://www.example.com/auth/facebook?omniauth_window_type=sameWindow&auth_origin_url=http%3A%2F%2Flocalhost%3A9876%2Foauth_callback'
499 | )
500 | })
501 | })
502 |
503 | describe('with an unsupported configuration', () => {
504 | beforeEach(() => {
505 | initService({
506 | oAuthWindowType: 'wrongValue'
507 | })
508 | })
509 |
510 | it('throws an error', () => {
511 | expect(() => service.signInOAuth('facebook')).toThrow(new Error('Unsupported oAuthWindowType "wrongValue"'))
512 | })
513 | })
514 | })
515 |
516 | describe('user signed out', () => {
517 | beforeEach(() => {
518 | initService({});
519 | });
520 |
521 | it('currentAuthData should return undefined', () => {
522 | expect(service.currentAuthData).toEqual(null);
523 | });
524 |
525 | it('currentUserData should return undefined', () => {
526 | expect(service.currentUserData).toEqual(null);
527 | });
528 |
529 | it('currentUserType should return undefined', () => {
530 | expect(service.currentUserType).toEqual(undefined);
531 | });
532 |
533 | it('userSignedIn should return false', () => {
534 | expect(service.userSignedIn()).toEqual(false);
535 | });
536 | });
537 |
538 | describe('user signed in', () => {
539 | beforeEach(() => {
540 | initService({});
541 | });
542 |
543 | it('currentAuthData should return current auth data', () => {
544 | service.signIn(signInData).subscribe(
545 | data => expect(service.currentAuthData).toEqual(authData)
546 | );
547 |
548 | const req = backend.expectOne({
549 | url: 'auth/sign_in',
550 | method: 'POST'
551 | });
552 |
553 | req.flush(userData, { headers: tokenHeaders });
554 | });
555 |
556 | /*it('currentUserData should return current user data', () => {
557 | service.signIn(signInData).subscribe(
558 | data => expect(service.currentUserData).toEqual(userData)
559 | );
560 |
561 | const req = backend.expectOne({
562 | url: 'auth/sign_in',
563 | method: 'POST'
564 | });
565 |
566 | req.flush( userData, { headers: tokenHeaders } );
567 | });*/
568 |
569 | it('userSignedIn should true', () => {
570 | service.signIn(signInData).subscribe(
571 | data => expect(service.userSignedIn()).toEqual(true)
572 | );
573 |
574 | const req = backend.expectOne({
575 | url: 'auth/sign_in',
576 | method: 'POST'
577 | });
578 |
579 | req.flush(userData, { headers: tokenHeaders });
580 | });
581 | });
582 | });
583 |
--------------------------------------------------------------------------------
/projects/angular-token/src/lib/angular-token.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Optional, Inject, PLATFORM_ID } from '@angular/core';
2 | import { ActivatedRoute, Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
3 | import { HttpClient, HttpResponse, HttpErrorResponse } from '@angular/common/http';
4 | import { isPlatformServer } from '@angular/common';
5 |
6 | import { Observable, fromEvent, interval, BehaviorSubject } from 'rxjs';
7 | import { pluck, filter, share, finalize } from 'rxjs/operators';
8 |
9 | import { ANGULAR_TOKEN_OPTIONS } from './angular-token.token';
10 |
11 | import {
12 | SignInData,
13 | RegisterData,
14 | UpdatePasswordData,
15 | ResetPasswordData,
16 |
17 | UserType,
18 | UserData,
19 | AuthData,
20 | ApiResponse,
21 |
22 | AngularTokenOptions,
23 |
24 | TokenPlatform,
25 | TokenInAppBrowser,
26 | } from './angular-token.model';
27 |
28 | @Injectable({
29 | providedIn: 'root',
30 | })
31 | export class AngularTokenService implements CanActivate {
32 |
33 | get currentUserType(): string {
34 | if (this.userType.value != null) {
35 | return this.userType.value.name;
36 | } else {
37 | return undefined;
38 | }
39 | }
40 |
41 | get currentUserData(): UserData {
42 | return this.userData.value;
43 | }
44 |
45 | get currentAuthData(): AuthData {
46 | return this.authData.value;
47 | }
48 |
49 | get apiBase(): string {
50 | console.warn('[angular-token] The attribute .apiBase will be removed in the next major release, please use' +
51 | '.tokenOptions.apiBase instead');
52 | return this.options.apiBase;
53 | }
54 |
55 | get tokenOptions(): AngularTokenOptions {
56 | return this.options;
57 | }
58 |
59 | set tokenOptions(options: AngularTokenOptions) {
60 | this.options = (Object).assign(this.options, options);
61 | }
62 |
63 | private options: AngularTokenOptions;
64 | public userType: BehaviorSubject = new BehaviorSubject(null);
65 | public authData: BehaviorSubject = new BehaviorSubject(null);
66 | public userData: BehaviorSubject = new BehaviorSubject(null);
67 | private global: Partial;
68 |
69 | private localStorage: Storage | any = {};
70 |
71 | constructor(
72 | private http: HttpClient,
73 | // "any" used here to assuage environments where type Window does not exist at build time.
74 | // It's assigned to global in the constructor which has the proper typing.
75 | @Inject('Window') private window: any,
76 | @Inject(ANGULAR_TOKEN_OPTIONS) config: any,
77 | @Inject(PLATFORM_ID) private platformId: Object,
78 | @Optional() private activatedRoute: ActivatedRoute,
79 | @Optional() private router: Router
80 | ) {
81 | this.global = (typeof this.window !== 'undefined') ? this.window : {};
82 |
83 | if (isPlatformServer(this.platformId)) {
84 |
85 | // Bad pratice, needs fixing
86 | this.global = {
87 | open: (): Window => null,
88 | location: {
89 | href: '/',
90 | origin: '/'
91 | } as any
92 | };
93 |
94 | // Bad pratice, needs fixing
95 | this.localStorage.setItem = (): void => null;
96 | this.localStorage.getItem = (): void => null;
97 | this.localStorage.removeItem = (): void => null;
98 | } else {
99 | this.localStorage = localStorage;
100 | }
101 |
102 | const defaultOptions: AngularTokenOptions = {
103 | apiPath: null,
104 | apiBase: null,
105 |
106 | signInPath: 'auth/sign_in',
107 | signInRedirect: null,
108 | signInStoredUrlStorageKey: null,
109 |
110 | signOutPath: 'auth/sign_out',
111 | validateTokenPath: 'auth/validate_token',
112 | signOutFailedValidate: false,
113 |
114 | registerAccountPath: 'auth',
115 | deleteAccountPath: 'auth',
116 | registerAccountCallback: this.global.location.href,
117 |
118 | updatePasswordPath: 'auth',
119 |
120 | resetPasswordPath: 'auth/password',
121 | resetPasswordCallback: this.global.location.href,
122 |
123 | userTypes: null,
124 | loginField: 'email',
125 |
126 | oAuthBase: this.global.location.origin,
127 | oAuthPaths: {
128 | github: 'auth/github'
129 | },
130 | oAuthCallbackPath: 'oauth_callback',
131 | oAuthWindowType: 'newWindow',
132 | oAuthWindowOptions: null,
133 |
134 | oAuthBrowserCallbacks: {
135 | github: 'auth/github/callback',
136 | },
137 | };
138 |
139 | const mergedOptions = (Object).assign(defaultOptions, config);
140 | this.options = mergedOptions;
141 |
142 | if (this.options.apiBase === null) {
143 | console.warn(`[angular-token] You have not configured 'apiBase', which may result in security issues. ` +
144 | `Please refer to the documentation at https://github.com/neroniaky/angular-token/wiki`);
145 | }
146 |
147 | this.tryLoadAuthData();
148 | }
149 |
150 | userSignedIn(): boolean {
151 | if (this.authData.value == null) {
152 | return false;
153 | } else {
154 | return true;
155 | }
156 | }
157 |
158 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
159 | if (this.userSignedIn()) {
160 | return true;
161 | } else {
162 | // Store current location in storage (usefull for redirection after signing in)
163 | if (this.options.signInStoredUrlStorageKey) {
164 | this.localStorage.setItem(
165 | this.options.signInStoredUrlStorageKey,
166 | state.url
167 | );
168 | }
169 |
170 | // Redirect user to sign in if signInRedirect is set
171 | if (this.router && this.options.signInRedirect) {
172 | this.router.navigate([this.options.signInRedirect]);
173 | }
174 |
175 | return false;
176 | }
177 | }
178 |
179 |
180 | /**
181 | *
182 | * Actions
183 | *
184 | */
185 |
186 | // Register request
187 | registerAccount(registerData: RegisterData, additionalData?: any): Observable {
188 |
189 | registerData = Object.assign({}, registerData);
190 |
191 | if (registerData.userType == null) {
192 | this.userType.next(null);
193 | } else {
194 | this.userType.next(this.getUserTypeByName(registerData.userType));
195 | delete registerData.userType;
196 | }
197 |
198 | if (
199 | registerData.password_confirmation == null &&
200 | registerData.passwordConfirmation != null
201 | ) {
202 | registerData.password_confirmation = registerData.passwordConfirmation;
203 | delete registerData.passwordConfirmation;
204 | }
205 |
206 | if (additionalData !== undefined) {
207 | registerData.additionalData = additionalData;
208 | }
209 |
210 | const login = registerData.login;
211 | delete registerData.login;
212 | registerData[this.options.loginField] = login;
213 |
214 | registerData.confirm_success_url = this.options.registerAccountCallback;
215 |
216 | return this.http.post(
217 | this.getServerPath() + this.options.registerAccountPath, registerData
218 | );
219 | }
220 |
221 | // Delete Account
222 | deleteAccount(): Observable {
223 | return this.http.delete(this.getServerPath() + this.options.deleteAccountPath);
224 | }
225 |
226 | // Sign in request and set storage
227 | signIn(signInData: SignInData, additionalData?: any): Observable {
228 | this.userType.next((signInData.userType == null) ? null : this.getUserTypeByName(signInData.userType));
229 |
230 | const body = {
231 | [this.options.loginField]: signInData.login,
232 | password: signInData.password
233 | };
234 |
235 | if (additionalData !== undefined) {
236 | body['additionalData'] = additionalData;
237 | }
238 |
239 | const observ = this.http.post(
240 | this.getServerPath() + this.options.signInPath, body
241 | ).pipe(share());
242 |
243 | observ.subscribe(res => this.userData.next(res.data));
244 |
245 | return observ;
246 | }
247 |
248 | signInOAuth(oAuthType: string, inAppBrowser?: TokenInAppBrowser, platform?: TokenPlatform) {
249 |
250 | const oAuthPath: string = this.getOAuthPath(oAuthType);
251 | const callbackUrl: string = new URL(this.options.oAuthCallbackPath, this.global.location.origin).href;
252 | const oAuthWindowType: string = this.options.oAuthWindowType;
253 | const authUrl: string = this.getOAuthUrl(oAuthPath, callbackUrl, oAuthWindowType);
254 |
255 | if (oAuthWindowType === 'newWindow' ||
256 | (oAuthWindowType == 'inAppBrowser' && (!platform || !platform.is('cordova') || !(platform.is('ios') || platform.is('android'))))) {
257 | const oAuthWindowOptions = this.options.oAuthWindowOptions;
258 | let windowOptions = '';
259 |
260 | if (oAuthWindowOptions) {
261 | for (const key in oAuthWindowOptions) {
262 | if (oAuthWindowOptions.hasOwnProperty(key)) {
263 | windowOptions += `,${key}=${oAuthWindowOptions[key]}`;
264 | }
265 | }
266 | }
267 |
268 | const popup = this.window.open(
269 | authUrl,
270 | '_blank',
271 | `closebuttoncaption=Cancel${windowOptions}`
272 | );
273 | return this.requestCredentialsViaPostMessage(popup);
274 | } else if (oAuthWindowType == 'inAppBrowser') {
275 | let oAuthBrowserCallback = this.options.oAuthBrowserCallbacks[oAuthType];
276 | if (!oAuthBrowserCallback) {
277 | throw new Error(`To login with oAuth provider ${oAuthType} using inAppBrowser the callback (in oAuthBrowserCallbacks) is required.`);
278 | }
279 | // let oAuthWindowOptions = this.options.oAuthWindowOptions;
280 | // let windowOptions = '';
281 |
282 | // if (oAuthWindowOptions) {
283 | // for (let key in oAuthWindowOptions) {
284 | // windowOptions += `,${key}=${oAuthWindowOptions[key]}`;
285 | // }
286 | // }
287 |
288 | let browser = inAppBrowser.create(
289 | authUrl,
290 | '_blank',
291 | 'location=no'
292 | );
293 |
294 | return new Observable((observer) => {
295 | browser.on('loadstop').subscribe((ev: any) => {
296 | if (ev.url.indexOf(oAuthBrowserCallback) > -1) {
297 | browser.executeScript({ code: "requestCredentials();" }).then((credentials: any) => {
298 | this.getAuthDataFromPostMessage(credentials[0]);
299 |
300 | let pollerObserv = interval(400);
301 |
302 | let pollerSubscription = pollerObserv.subscribe(() => {
303 | if (this.userSignedIn()) {
304 | observer.next(this.authData);
305 | observer.complete();
306 |
307 | pollerSubscription.unsubscribe();
308 | browser.close();
309 | }
310 | }, (error: any) => {
311 | observer.error(error);
312 | observer.complete();
313 | });
314 | }, (error: any) => {
315 | observer.error(error);
316 | observer.complete();
317 | });
318 | }
319 | }, (error: any) => {
320 | observer.error(error);
321 | observer.complete();
322 | });
323 | })
324 | } else if (oAuthWindowType === 'sameWindow') {
325 | this.global.location.href = authUrl;
326 | return undefined;
327 | } else {
328 | throw new Error(`Unsupported oAuthWindowType "${oAuthWindowType}"`);
329 | }
330 | }
331 |
332 | processOAuthCallback(): void {
333 | this.getAuthDataFromParams();
334 | }
335 |
336 | // Sign out request and delete storage
337 | signOut(): Observable {
338 | return this.http.delete(this.getServerPath() + this.options.signOutPath)
339 | // Only remove the localStorage and clear the data after the call
340 | .pipe(
341 | finalize(() => {
342 | this.localStorage.removeItem('accessToken');
343 | this.localStorage.removeItem('client');
344 | this.localStorage.removeItem('expiry');
345 | this.localStorage.removeItem('tokenType');
346 | this.localStorage.removeItem('uid');
347 |
348 | this.authData.next(null);
349 | this.userType.next(null);
350 | this.userData.next(null);
351 | }
352 | )
353 | );
354 | }
355 |
356 | // Validate token request
357 | validateToken(): Observable {
358 | const observ = this.http.get(
359 | this.getServerPath() + this.options.validateTokenPath
360 | ).pipe(share());
361 |
362 | observ.subscribe(
363 | (res) => this.userData.next(res.data),
364 | (error) => {
365 | if (error.status === 401 && this.options.signOutFailedValidate) {
366 | this.signOut();
367 | }
368 | });
369 |
370 | return observ;
371 | }
372 |
373 | // Update password request
374 | updatePassword(updatePasswordData: UpdatePasswordData): Observable {
375 |
376 | if (updatePasswordData.userType != null) {
377 | this.userType.next(this.getUserTypeByName(updatePasswordData.userType));
378 | }
379 |
380 | let args: any;
381 |
382 | if (updatePasswordData.passwordCurrent == null) {
383 | args = {
384 | password: updatePasswordData.password,
385 | password_confirmation: updatePasswordData.passwordConfirmation
386 | };
387 | } else {
388 | args = {
389 | current_password: updatePasswordData.passwordCurrent,
390 | password: updatePasswordData.password,
391 | password_confirmation: updatePasswordData.passwordConfirmation
392 | };
393 | }
394 |
395 | if (updatePasswordData.resetPasswordToken) {
396 | args.reset_password_token = updatePasswordData.resetPasswordToken;
397 | }
398 |
399 | const body = args;
400 | return this.http.put(this.getServerPath() + this.options.updatePasswordPath, body);
401 | }
402 |
403 | // Reset password request
404 | resetPassword(resetPasswordData: ResetPasswordData, additionalData?: any): Observable {
405 |
406 |
407 | if (additionalData !== undefined) {
408 | resetPasswordData.additionalData = additionalData;
409 | }
410 |
411 | this.userType.next(
412 | (resetPasswordData.userType == null) ? null : this.getUserTypeByName(resetPasswordData.userType)
413 | );
414 |
415 | const body = {
416 | [this.options.loginField]: resetPasswordData.login,
417 | redirect_url: this.options.resetPasswordCallback
418 | };
419 |
420 | return this.http.post(this.getServerPath() + this.options.resetPasswordPath, body);
421 | }
422 |
423 |
424 | /**
425 | *
426 | * Construct Paths / Urls
427 | *
428 | */
429 |
430 | private getUserPath(): string {
431 | return (this.userType.value == null) ? '' : this.userType.value.path + '/';
432 | }
433 |
434 | private addTrailingSlashIfNeeded(url: string): string {
435 | const lastChar = url[url.length - 1];
436 |
437 | return lastChar === '/' ? url : url + '/';
438 | }
439 |
440 | private getApiPath(): string {
441 | let constructedPath = '';
442 |
443 | if (this.options.apiBase != null) {
444 | constructedPath += this.addTrailingSlashIfNeeded(this.options.apiBase);
445 | }
446 |
447 | if (this.options.apiPath != null) {
448 | constructedPath += this.addTrailingSlashIfNeeded(this.options.apiPath);
449 | }
450 |
451 | return constructedPath;
452 | }
453 |
454 | private getServerPath(): string {
455 | return this.getApiPath() + this.getUserPath();
456 | }
457 |
458 | private getOAuthPath(oAuthType: string): string {
459 | let oAuthPath: string;
460 |
461 | oAuthPath = this.options.oAuthPaths[oAuthType];
462 |
463 | if (oAuthPath == null) {
464 | oAuthPath = `/auth/${oAuthType}`;
465 | }
466 |
467 | return oAuthPath;
468 | }
469 |
470 | private getOAuthUrl(oAuthPath: string, callbackUrl: string, windowType: string): string {
471 | let url = new URL(
472 | `${oAuthPath}?omniauth_window_type=${windowType}&auth_origin_url=${encodeURIComponent(callbackUrl)}`,
473 | this.options.oAuthBase
474 | ).href
475 |
476 | if (this.userType.value != null) {
477 | url += `&resource_class=${this.userType.value.name}`;
478 | }
479 |
480 | return url;
481 | }
482 |
483 |
484 | /**
485 | *
486 | * Get Auth Data
487 | *
488 | */
489 |
490 | // Try to load auth data
491 | private tryLoadAuthData(): void {
492 |
493 | const userType = this.getUserTypeByName(this.localStorage.getItem('userType'));
494 |
495 | if (userType) {
496 | this.userType.next(userType);
497 | }
498 |
499 | this.getAuthDataFromStorage();
500 |
501 | if (this.activatedRoute) {
502 | this.getAuthDataFromParams();
503 | }
504 |
505 | // if (this.authData) {
506 | // this.validateToken();
507 | // }
508 | }
509 |
510 | // Parse Auth data from response
511 | public getAuthHeadersFromResponse(data: HttpResponse | HttpErrorResponse): void {
512 | const headers = data.headers;
513 |
514 | const authData: AuthData = {
515 | accessToken: headers.get('access-token'),
516 | client: headers.get('client'),
517 | expiry: headers.get('expiry'),
518 | tokenType: headers.get('token-type'),
519 | uid: headers.get('uid')
520 | };
521 |
522 | this.setAuthData(authData);
523 | }
524 |
525 | // Parse Auth data from post message
526 | private getAuthDataFromPostMessage(data: any): void {
527 | const authData: AuthData = {
528 | accessToken: data['auth_token'],
529 | client: data['client_id'],
530 | expiry: data['expiry'],
531 | tokenType: 'Bearer',
532 | uid: data['uid']
533 | };
534 |
535 | this.setAuthData(authData);
536 | }
537 |
538 | // Try to get auth data from storage.
539 | public getAuthDataFromStorage(): void {
540 |
541 | const authData: AuthData = {
542 | accessToken: this.localStorage.getItem('accessToken'),
543 | client: this.localStorage.getItem('client'),
544 | expiry: this.localStorage.getItem('expiry'),
545 | tokenType: this.localStorage.getItem('tokenType'),
546 | uid: this.localStorage.getItem('uid')
547 | };
548 |
549 | if (this.checkAuthData(authData)) {
550 | this.authData.next(authData);
551 | }
552 | }
553 |
554 | // Try to get auth data from url parameters.
555 | private getAuthDataFromParams(): void {
556 | this.activatedRoute.queryParams.subscribe(queryParams => {
557 | const authData: AuthData = {
558 | accessToken: queryParams['token'] || queryParams['auth_token'],
559 | client: queryParams['client_id'],
560 | expiry: queryParams['expiry'],
561 | tokenType: 'Bearer',
562 | uid: queryParams['uid']
563 | };
564 |
565 | if (this.checkAuthData(authData)) {
566 | this.authData.next(authData);
567 | }
568 | });
569 | }
570 |
571 | /**
572 | *
573 | * Set Auth Data
574 | *
575 | */
576 |
577 | // Write auth data to storage
578 | private setAuthData(authData: AuthData): void {
579 | if (this.checkAuthData(authData)) {
580 |
581 | this.authData.next(authData);
582 |
583 | this.localStorage.setItem('accessToken', authData.accessToken);
584 | this.localStorage.setItem('client', authData.client);
585 | this.localStorage.setItem('expiry', authData.expiry);
586 | this.localStorage.setItem('tokenType', authData.tokenType);
587 | this.localStorage.setItem('uid', authData.uid);
588 |
589 | if (this.userType.value != null) {
590 | this.localStorage.setItem('userType', this.userType.value.name);
591 | }
592 |
593 | }
594 | }
595 |
596 |
597 | /**
598 | *
599 | * Validate Auth Data
600 | *
601 | */
602 |
603 | // Check if auth data complete and if response token is newer
604 | private checkAuthData(authData: AuthData): boolean {
605 |
606 | if (
607 | authData.accessToken != null &&
608 | authData.client != null &&
609 | authData.expiry != null &&
610 | authData.tokenType != null &&
611 | authData.uid != null
612 | ) {
613 | if (this.authData.value != null) {
614 | return authData.expiry >= this.authData.value.expiry;
615 | }
616 | return true;
617 | }
618 | return false;
619 | }
620 |
621 |
622 | /**
623 | *
624 | * OAuth
625 | *
626 | */
627 |
628 | private requestCredentialsViaPostMessage(authWindow: any): Observable {
629 | const pollerObserv = interval(500);
630 |
631 | const responseObserv = fromEvent(this.global as any, 'message').pipe(
632 | pluck('data'),
633 | filter(this.oAuthWindowResponseFilter)
634 | );
635 |
636 | responseObserv.subscribe(
637 | this.getAuthDataFromPostMessage.bind(this)
638 | );
639 |
640 | const pollerSubscription = pollerObserv.subscribe(() => {
641 | if (authWindow.closed) {
642 | pollerSubscription.unsubscribe();
643 | } else {
644 | authWindow.postMessage('requestCredentials', '*');
645 | }
646 | });
647 |
648 | return responseObserv;
649 | }
650 |
651 | private oAuthWindowResponseFilter(data: any): any {
652 | if (data.message === 'deliverCredentials' || data.message === 'authFailure') {
653 | return data;
654 | }
655 | }
656 |
657 |
658 | /**
659 | *
660 | * Utilities
661 | *
662 | */
663 |
664 | // Match user config by user config name
665 | private getUserTypeByName(name: string): UserType {
666 | if (name == null || this.options.userTypes == null) {
667 | return null;
668 | }
669 |
670 | return this.options.userTypes.find(
671 | userType => userType.name === name
672 | );
673 | }
674 | }
675 |
--------------------------------------------------------------------------------