Skip to content
This repository has been archived by the owner on Jul 26, 2022. It is now read-only.

[okta-angular]: adds child route guard OKTA-283009 #776

Merged
merged 2 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/okta-angular/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# PENDING

### Features

[#776](https://github.com/okta/okta-oidc-js/pull/776) `OktaAuthGuard` now implements `canActivateChild`

# 2.0.0

[#690](https://github.com/okta/okta-oidc-js/pull/690)
Expand Down
32 changes: 30 additions & 2 deletions packages/okta-angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ For PKCE flow, this should be left undefined or set to `['code']`.

The top-level Angular module which provides these components and services:

- [`OktaAuthGuard`](#oktaauthguard) - A navigation guard using [CanActivate](https://angular.io/api/router/CanActivate) to grant access to a page only after successful authentication.
- [`OktaAuthGuard`](#oktaauthguard) - A navigation guard implementing [CanActivate](https://angular.io/api/router/CanActivate) and [CanActivateChild](https://angular.io/api/router/CanActivateChild) to grant access to a page (and/or its children) only after successful authentication.
- [`OktaCallbackComponent`](#oktacallbackcomponent) - Handles the implicit flow callback by parsing tokens from the URL and storing them automatically.
- [`OktaLoginRedirectComponent`](#oktaloginredirectcomponent) - Redirects users to the Okta Hosted Login Page for authentication.
- [`OktaAuthService`](#oktaauthservice) - Highest-level service containing the `okta-angular` public methods.
Expand All @@ -149,7 +149,35 @@ const appRoutes: Routes = [
{
path: 'protected',
component: MyProtectedComponent,
canActivate: [ OktaAuthGuard ]
canActivate: [ OktaAuthGuard ],
children: [{
// children of a protected route are also protected
path: 'also-protected'
}]
},
...
]
```

You can use `canActivateChild` to protect children of an unprotected route:

```typescript
// myApp.module.ts

import {
OktaAuthGuard,
...
} from '@okta/okta-angular';

const appRoutes: Routes = [
{
path: 'public',
component: MyPublicComponent,
canActivateChild: [ OktaAuthGuard ],
children: [{
path: 'protected',
component: MyProtectedComponent
}]
},
...
]
Expand Down
11 changes: 10 additions & 1 deletion packages/okta-angular/src/okta/okta.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { Injectable, Injector } from '@angular/core';
import {
CanActivate,
CanActivateChild,
ActivatedRouteSnapshot,
RouterStateSnapshot,
} from '@angular/router';
Expand All @@ -21,7 +22,7 @@ import { OktaAuthService } from './services/okta.service';
import { AuthRequiredFunction } from './models/okta.config';

@Injectable()
export class OktaAuthGuard implements CanActivate {
export class OktaAuthGuard implements CanActivate, CanActivateChild {
constructor(private oktaAuth: OktaAuthService, private injector: Injector) { }

/**
Expand Down Expand Up @@ -54,4 +55,12 @@ export class OktaAuthGuard implements CanActivate {

return false;
}

async canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) {
return this.canActivate(route, state);
}

}
33 changes: 31 additions & 2 deletions packages/okta-angular/test/e2e/harness/e2e/app.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
OktaSignInPage,
LoginPage,
ProtectedPage,
PublicPage,
SessionTokenSignInPage
} from './page-objects';

Expand All @@ -29,13 +30,15 @@ describe('Angular + Okta App', () => {
let loginPage: LoginPage;
let protectedPage: ProtectedPage;
let sessionTokenSignInPage: SessionTokenSignInPage;
let publicPage: PublicPage;

beforeEach(() => {
page = new AppPage();
loginPage = new LoginPage();
oktaLoginPage = new OktaSignInPage();
protectedPage = new ProtectedPage();
sessionTokenSignInPage = new SessionTokenSignInPage();
publicPage = new PublicPage();
});

describe('implicit flow', () => {
Expand Down Expand Up @@ -90,7 +93,7 @@ describe('Angular + Okta App', () => {
password: environment.PASSWORD
});

loginPage.waitUntilVisible();
loginPage.waitUntilLoggedIn();
expect(loginPage.getLogoutButton().isPresent()).toBeTruthy();

// Logout
Expand Down Expand Up @@ -153,7 +156,7 @@ describe('Angular + Okta App', () => {
password: environment.PASSWORD
});

loginPage.waitUntilVisible();
loginPage.waitUntilLoggedIn();
expect(loginPage.getLogoutButton().isPresent()).toBeTruthy();

// Logout
Expand Down Expand Up @@ -185,6 +188,32 @@ describe('Angular + Okta App', () => {
});
});

describe('child route guard', () => {
it('displays the parent route without authentication', () => {
publicPage.navigateTo();
publicPage.waitUntilVisible();
expect(publicPage.getLoginButton().isPresent()).toBeTruthy();
expect(publicPage.getPublicArea().isPresent()).toBeTruthy();
publicPage.waitUntilTextVisible('public-message', 'Public!');
expect(publicPage.getPrivateArea().isPresent()).toBeFalsy();
});

it('displays the child route with authentication', () => {
publicPage.navigateTo('/private');

oktaLoginPage.waitUntilVisible(environment.ISSUER);
oktaLoginPage.signIn({
username: environment.USERNAME,
password: environment.PASSWORD
});

publicPage.waitUntilVisible();
expect(page.getLogoutButton().isPresent()).toBeTruthy();
expect(publicPage.getPublicArea().isPresent()).toBeTruthy();
publicPage.waitUntilTextVisible('public-message', 'Public!');
expect(publicPage.getPrivateArea().isPresent()).toBeTruthy();
publicPage.waitUntilTextVisible('userinfo-container', 'email');
});
});

});
1 change: 1 addition & 0 deletions packages/okta-angular/test/e2e/harness/e2e/page-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export { OktaSignInPage } from './page-objects/okta-signin.po';
export { LoginPage } from './page-objects/login.po';
export { ProtectedPage } from './page-objects/protected.po';
export { SessionTokenSignInPage } from './page-objects/sessionToken-signin.po';
export { PublicPage } from './page-objects/public.po';
16 changes: 14 additions & 2 deletions packages/okta-angular/test/e2e/harness/e2e/page-objects/app.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@
import { browser, by, element, ExpectedConditions } from 'protractor';

export class AppPage {
public path;

constructor(path: string = '/') {
this.path = path;
}

navigateTo(query: string = '') {
return browser.get('/' + query);
return browser.get(this.path + query);
}

waitUntilVisible() {
waitUntilVisible(query: string = '') {
browser.wait(ExpectedConditions.urlContains(this.path + query), 20000);
const loginExists = ExpectedConditions.presenceOf(this.getLoginButton());
const logoutExists = ExpectedConditions.presenceOf(this.getLogoutButton());
browser.wait(ExpectedConditions.or(loginExists, logoutExists), 5000);
Expand All @@ -31,6 +38,11 @@ export class AppPage {
browser.wait(ExpectedConditions.presenceOf(this.getLogoutButton()), 20000);
}

waitUntilTextVisible(id: string, text: string) {
const el = element(by.id(id));
browser.wait(ExpectedConditions.textToBePresentInElement(el, text), 5000);
}

waitForElement(id: string) {
const el = element(by.id(id));
browser.wait(ExpectedConditions.presenceOf(el), 5000);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,11 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

import { browser, element, by, ExpectedConditions } from 'protractor';
import { browser, ExpectedConditions } from 'protractor';
import { AppPage } from './app.po';

export class LoginPage {
navigateTo() {
return browser.get('/login');
}

waitUntilVisible() {
browser.wait(ExpectedConditions.presenceOf(this.getLogoutButton()), 5000);
}

waitForElement(id: string) {
const el = element(by.id(id));
browser.wait(ExpectedConditions.presenceOf(el), 5000);
}

getLoginButton() {
return element(by.id('login-button'));
}

getLogoutButton() {
return element(by.id('logout-button'));
export class LoginPage extends AppPage {
constructor() {
super('/login');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,12 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

import { browser, element, by, ExpectedConditions } from 'protractor';
import { element, by } from 'protractor';
import { AppPage } from './app.po';

export class ProtectedPage {

navigateTo(query: string = '') {
return browser.get('/protected' + query);
}

waitUntilVisible(query: string = '') {
browser.wait(ExpectedConditions.urlContains('/protected' + query), 20000);
}

waitUntilTextVisible(id: string, text: string) {
const el = element(by.id(id));
browser.wait(ExpectedConditions.textToBePresentInElement(el, text), 5000);
}

waitForElement(id: string) {
const el = element(by.id(id));
browser.wait(ExpectedConditions.presenceOf(el), 5000);
}

getLogoutButton() {
return element(by.id('logout-button'));
}

getLoginButton() {
return element(by.id('login-button'));
export class ProtectedPage extends AppPage {
constructor() {
super('/protected');
}

getUserInfo() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*!
* Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/

import { element, by } from 'protractor';
import { AppPage } from './app.po';

export class PublicPage extends AppPage {
constructor() {
super('/public');
}

getPublicArea() {
return element(by.tagName('app-public'));
}

getPrivateArea() {
return element(by.tagName('app-secure'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import { OktaAuthService } from '@okta/okta-angular';
<button id="protected-button" routerLink="/protected" [queryParams]="{ fooParams: 'foo' }"> Protected </button>
<button id="protected-login-button" routerLink="/protected-with-data"
[queryParams]="{ fooParams: 'foo' }"> Protected Page w/ custom config </button>

<button id="public-button" routerLink="/public"> Parent Route (public) </button>
<button id="private-button" routerLink="/public/private"> Child Route (private) </button>
<router-outlet></router-outlet>
`,
})
Expand Down
19 changes: 16 additions & 3 deletions packages/okta-angular/test/e2e/harness/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import { ProtectedComponent } from './protected.component';
import { AppComponent } from './app.component';
import { SessionTokenLoginComponent } from './sessionToken-login.component';
import { PublicComponent } from './public.component';

export function onNeedsAuthenticationGuard(oktaAuth: OktaAuthService, injector: Injector) {
const router = injector.get(Router);
Expand Down Expand Up @@ -69,8 +70,8 @@ const appRoutes: Routes = [
children: [
{
path: 'foo',
component: ProtectedComponent,
canActivate: [ OktaAuthGuard ]
component: ProtectedComponent
// protected by canActivate on parent route
}
]
},
Expand All @@ -81,6 +82,17 @@ const appRoutes: Routes = [
data: {
onAuthRequired: onNeedsAuthenticationGuard
}
},
{
path: 'public',
component: PublicComponent,
canActivateChild: [ OktaAuthGuard ],
children: [
{
path: 'private',
component: ProtectedComponent
}
]
}
];

Expand Down Expand Up @@ -117,7 +129,8 @@ if (environment.OKTA_TESTING_DISABLEHTTPSCHECK) {
declarations: [
AppComponent,
ProtectedComponent,
SessionTokenLoginComponent
SessionTokenLoginComponent,
PublicComponent
],
providers: [{
provide: OKTA_CONFIG,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import { OktaAuthService } from '@okta/okta-angular';
@Component({
selector: 'app-secure',
template: `
{{ message }}
<div>
{{ message }}<br/>
<pre id="userinfo-container">{{ user }}</pre>
`
</div>`
})
export class ProtectedComponent implements OnInit {
message;
Expand Down
Loading