-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(auth-guard): AngularFire auth guards (#2016)
- Loading branch information
1 parent
be0a1fb
commit e32164d
Showing
19 changed files
with
323 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
# Route users with AngularFire guards | ||
|
||
`AngularFireAuthGuard` provides a prebuilt [`canActivate` Router Guard](https://angular.io/api/router/CanActivate) using `AngularFireAuth`. By default unauthenticated users are not permitted to navigate to protected routes: | ||
|
||
```ts | ||
import { AngularFireAuthGuard } from '@angular/fire/auth-guard'; | ||
|
||
export const routes: Routes = [ | ||
{ path: '', component: AppComponent }, | ||
{ path: 'items', component: ItemListComponent, canActivate: [AngularFireAuthGuard] }, | ||
] | ||
``` | ||
|
||
## Customizing the behavior of `AngularFireAuthGuard` | ||
|
||
To customize the behavior of `AngularFireAuthGuard`, you can pass an RXJS pipe through the route data's `authGuardPipe` key. | ||
|
||
The `auth-guard` module provides the following pre-built pipes: | ||
|
||
| Exported pipe | Functionality | | ||
|-|-| | ||
| `loggedIn` | The default pipe, rejects if the user is not authenticated. | | ||
| `isNotAnonymous` | Rejects if the user is anonymous | | ||
| `emailVerified` | Rejects if the user's email is not verified | | ||
| `hasCustomClaim(claim)` | Rejects if the user does not have the specified claim | | ||
| `redirectUnauthorizedTo(redirect)` | Redirect unauthenticated users to a different route | | ||
| `redirectLoggedInTo(redirect)` | Redirect authenticated users to a different route | | ||
|
||
Example use: | ||
|
||
```ts | ||
import { AngularFireAuthGuard, hasCustomClaim, redirectUnauthorizedTo, redirectLoggedInTo } from '@angular/fire/auth-guard'; | ||
|
||
const adminOnly = hasCustomClaim('admin'); | ||
const redirectUnauthorizedToLogin = redirectUnauthorizedTo(['login']); | ||
const redirectLoggedInToItems = redirectLoggedInTo(['items']); | ||
const belongsToAccount = (next) => hasCustomClaim(`account-${next.params.id}`); | ||
|
||
export const routes: Routes = [ | ||
{ path: '', component: AppComponent }, | ||
{ path: 'login', component: LoginComponent, canActivate: [AngularFireAuthGuard], data: { authGuardPipe: redirectLoggedInToItems }}, | ||
{ path: 'items', component: ItemListComponent, canActivate: [AngularFireAuthGuard], data: { authGuardPipe: redirectUnauthorizedToLogin }, | ||
{ path: 'admin', component: AdminComponent, canActivate: [AngularFireAuthGuard], data: { authGuardPipe: adminOnly }}, | ||
{ path: 'accounts/:id', component: AdminComponent, canActivate: [AngularFireAuthGuard], data: { authGuardPipe: belongsToAccount }} | ||
]; | ||
``` | ||
Use the provided `canActivate` helper and spread syntax to make your routes more readable: | ||
```ts | ||
import { canActivate } from '@angular/fire/auth-guard'; | ||
|
||
export const routes: Routes = [ | ||
{ path: '', component: AppComponent }, | ||
{ path: 'login', component: LoginComponent, ...canActivate(redirectLoggedInToItems) }, | ||
{ path: 'items', component: ItemListComponent, ...canActivate(redirectUnauthorizedToLogin) }, | ||
{ path: 'admin', component: AdminComponent, ...canActivate(adminOnly) }, | ||
{ path: 'accounts/:id', component: AdminComponent, ...canActivate(belongsToAccount) } | ||
]; | ||
``` | ||
### Compose your own pipes | ||
`AngularFireAuthGuard` pipes are RXJS operators which transform an optional User to a boolean or Array (for redirects). You can build easily build your own to customize behavior further: | ||
```ts | ||
import { map } from 'rxjs/operators'; | ||
|
||
// This pipe redirects a user to their "profile edit" page or the "login page" if they're unauthenticated | ||
// { path: 'profile', ...canActivate(redirectToProfileEditOrLogin) } | ||
const redirectToProfileEditOrLogin = map(user => user ? ['profiles', user.uid, 'edit'] : ['login']); | ||
``` | ||
The `auth-guard` modules provides a `customClaims` operator to reduce boiler plate when checking a user's claims: | ||
```ts | ||
import { pipe } from 'rxjs'; | ||
import { map } from 'rxjs/operators'; | ||
import { customClaims } from '@angular/fire/auth-guard'; | ||
|
||
// This pipe will only allow users with the editor role to access the route | ||
// { path: 'articles/:id/edit', component: ArticleEditComponent, ...canActivate(editorOnly) } | ||
const editorOnly = pipe(customClaims, map(claims => claims.role === "editor")); | ||
``` | ||
### Using router state | ||
`AngularFireAuthGuard` will also accept `AuthPipeGenerator`s which generate `AuthPipe`s given the router state: | ||
```ts | ||
import { pipe } from 'rxjs'; | ||
import { map } from 'rxjs/operators'; | ||
import { customClaims } from '@angular/fire/auth-guard'; | ||
|
||
// Only allow navigation to the route if :userId matches the authenticated user's uid | ||
// { path: 'user/:userId/edit', component: ProfileEditComponent, ...canActivate(onlyAllowSelf) } | ||
const onlyAllowSelf = (next) => map(user => !!user && next.params.userId === user.uid); | ||
|
||
// Only allow navigation to the route if the user has a custom claim matching :accountId | ||
// { path: 'accounts/:accountId/billing', component: BillingDetailsComponent, ...canActivate(accountAdmin) } | ||
const accountAdmin = (next) => pipe(customClaims, map(claims => claims[`account-${next.params.accountId}-role`] === "admin")); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { NgModule } from '@angular/core'; | ||
import { AngularFireAuthGuard } from './auth-guard'; | ||
|
||
@NgModule({ | ||
providers: [ AngularFireAuthGuard ] | ||
}) | ||
export class AngularFireAuthGuardModule { } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { TestBed, inject } from '@angular/core/testing'; | ||
import { FirebaseApp, AngularFireModule } from '@angular/fire'; | ||
import { COMMON_CONFIG } from './test-config'; | ||
import { AngularFireAuthModule } from '@angular/fire/auth'; | ||
import { AngularFireAuthGuardModule, AngularFireAuthGuard } from '@angular/fire/auth-guard'; | ||
import { RouterModule, Router } from '@angular/router'; | ||
import { APP_BASE_HREF } from '@angular/common'; | ||
|
||
describe('AngularFireAuthGuard', () => { | ||
let app: FirebaseApp; | ||
let router: Router; | ||
|
||
beforeEach(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [ | ||
AngularFireModule.initializeApp(COMMON_CONFIG), | ||
AngularFireAuthModule, | ||
AngularFireAuthGuardModule, | ||
RouterModule.forRoot([ | ||
{ path: 'a', redirectTo: '/', canActivate: [AngularFireAuthGuard] } | ||
]) | ||
], | ||
providers: [ | ||
{ provide: APP_BASE_HREF, useValue: 'http://localhost:4200/' } | ||
] | ||
}); | ||
inject([FirebaseApp, Router], (app_: FirebaseApp, router_: Router) => { | ||
app = app_; | ||
router = router_; | ||
})(); | ||
}); | ||
|
||
afterEach(done => { | ||
app.delete().then(done, done.fail); | ||
}); | ||
|
||
it('should be injectable', () => { | ||
expect(router).toBeTruthy(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { Injectable, InjectionToken } from '@angular/core'; | ||
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; | ||
import { Observable, of, pipe, UnaryFunction } from 'rxjs'; | ||
import { map, switchMap, take } from 'rxjs/operators' | ||
import { User, auth } from 'firebase/app'; | ||
import { AngularFireAuth } from '@angular/fire/auth'; | ||
|
||
export type AuthPipeGenerator = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => AuthPipe; | ||
export type AuthPipe = UnaryFunction<Observable<User|null>, Observable<boolean|any[]>>; | ||
|
||
@Injectable() | ||
export class AngularFireAuthGuard implements CanActivate { | ||
|
||
constructor(private afAuth: AngularFireAuth, private router: Router) {} | ||
|
||
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | ||
const authPipeFactory: AuthPipeGenerator = next.data.authGuardPipe || (() => loggedIn); | ||
return this.afAuth.user.pipe( | ||
take(1), | ||
authPipeFactory(next, state), | ||
map(canActivate => typeof canActivate == "boolean" ? canActivate : this.router.createUrlTree(canActivate)) | ||
); | ||
} | ||
|
||
} | ||
|
||
export const canActivate = (pipe: AuthPipe|AuthPipeGenerator) => ({ | ||
canActivate: [ AngularFireAuthGuard ], data: { authGuardPipe: pipe.name === "" ? pipe : () => pipe} | ||
}); | ||
|
||
export const loggedIn: AuthPipe = map(user => !!user); | ||
export const isNotAnonymous: AuthPipe = map(user => !!user && !user.isAnonymous); | ||
export const idTokenResult = switchMap((user: User|null) => user ? user.getIdTokenResult() : of(null)); | ||
export const emailVerified: AuthPipe = map(user => !!user && user.emailVerified); | ||
export const customClaims = pipe(idTokenResult, map(idTokenResult => idTokenResult ? idTokenResult.claims : [])); | ||
export const hasCustomClaim = (claim:string) => pipe(customClaims, map(claims => claims.hasOwnProperty(claim))); | ||
export const redirectUnauthorizedTo = (redirect: any[]) => pipe(loggedIn, map(loggedIn => loggedIn || redirect)); | ||
export const redirectLoggedInTo = (redirect: any[]) => pipe(loggedIn, map(loggedIn => loggedIn && redirect || true)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
import './auth-guard.spec'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './public_api'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"name": "@angular/fire/auth-guard", | ||
"version": "ANGULARFIRE2_VERSION", | ||
"description": "The auth guard module", | ||
"main": "../bundles/auth-guard.umd.js", | ||
"module": "index.js", | ||
"es2015": "./es2015/index.js", | ||
"keywords": [ | ||
"angular", | ||
"firebase", | ||
"rxjs" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/angular/angularfire2.git" | ||
}, | ||
"author": "angular,firebase", | ||
"license": "MIT", | ||
"peerDependencies": { | ||
"@angular/fire": "ANGULARFIRE2_VERSION", | ||
"@angular/common": "ANGULAR_VERSION", | ||
"@angular/core": "ANGULAR_VERSION", | ||
"@angular/platform-browser": "ANGULAR_VERSION", | ||
"@angular/platform-browser-dynamic": "ANGULAR_VERSION", | ||
"@angular/router": "ANGULAR_VERSION", | ||
"firebase": "FIREBASE_VERSION", | ||
"rxjs": "RXJS_VERSION", | ||
"zone.js": "ZONEJS_VERSION" | ||
}, | ||
"typings": "index.d.ts" | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './auth-guard'; | ||
export * from './auth-guard.module'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
|
||
export const COMMON_CONFIG = { | ||
apiKey: "AIzaSyBVSy3YpkVGiKXbbxeK0qBnu3-MNZ9UIjA", | ||
authDomain: "angularfire2-test.firebaseapp.com", | ||
databaseURL: "https://angularfire2-test.firebaseio.com", | ||
storageBucket: "angularfire2-test.appspot.com", | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{ | ||
"compilerOptions": { | ||
"baseUrl": ".", | ||
"experimentalDecorators": true, | ||
"emitDecoratorMetadata": true, | ||
"module": "es2015", | ||
"target": "es2015", | ||
"noImplicitAny": false, | ||
"outDir": "../../dist/packages-dist/auth-guard/es2015", | ||
"rootDir": ".", | ||
"sourceMap": true, | ||
"inlineSources": true, | ||
"declaration": false, | ||
"removeComments": true, | ||
"strictNullChecks": true, | ||
"lib": ["es2015", "dom", "es2015.promise", "es2015.collection", "es2015.iterable"], | ||
"skipLibCheck": true, | ||
"moduleResolution": "node", | ||
"paths": { | ||
"@angular/fire": ["../../dist/packages-dist"], | ||
"@angular/fire/auth": ["../../dist/packages-dist/auth"] | ||
} | ||
}, | ||
"files": [ | ||
"index.ts", | ||
"../../node_modules/zone.js/dist/zone.js.d.ts" | ||
], | ||
"angularCompilerOptions": { | ||
"skipTemplateCodegen": true, | ||
"strictMetadataEmit": true, | ||
"enableSummariesForJit": false | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"extends": "./tsconfig-build.json", | ||
"compilerOptions": { | ||
"target": "es5", | ||
"outDir": "../../dist/packages-dist/auth-guard", | ||
"declaration": true | ||
}, | ||
"files": [ | ||
"public_api.ts", | ||
"../../node_modules/zone.js/dist/zone.js.d.ts" | ||
], | ||
"angularCompilerOptions": { | ||
"skipTemplateCodegen": true, | ||
"strictMetadataEmit": true, | ||
"enableSummariesForJit": false, | ||
"flatModuleOutFile": "index.js", | ||
"flatModuleId": "@angular/fire/auth-guard" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"extends": "./tsconfig-esm.json", | ||
"compilerOptions": { | ||
"baseUrl": ".", | ||
"paths": { | ||
"@angular/fire": ["../../dist/packages-dist"], | ||
"@angular/fire/auth": ["../../dist/packages-dist/auth"], | ||
"@angular/fire/auth-guard": ["../../dist/packages-dist/auth-guard"] | ||
} | ||
}, | ||
"files": [ | ||
"index.spec.ts", | ||
"../../node_modules/zone.js/dist/zone.js.d.ts" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.