Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing autologin on not guarded route #1015

Merged
merged 8 commits into from
Mar 12, 2021
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
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
## Angular Lib for OpenID Connect/OAuth2 Changelog

### 2021-03-14 Version 11.6.4

- Improve AutoLoginGuard
- [PR](https://github.com/damienbod/angular-auth-oidc-client/pull/1015)

### 2021-03-12 Version 11.6.3

- Inconsistent behavior of OidcSecurityService.userData$ Observable, if autoUserinfo is false
- [PR](https://github.com/damienbod/angular-auth-oidc-client/pull/1008),
- [PR](https://github.com/damienbod/angular-auth-oidc-client/pull/1008)
- CheckSessionService keeps polling after logoffLocal() is invoked
- [PR](https://github.com/damienbod/angular-auth-oidc-client/pull/1009),
- [PR](https://github.com/damienbod/angular-auth-oidc-client/pull/1009)

### 2021-03-05 Version 11.6.2

Expand Down
45 changes: 41 additions & 4 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,61 @@ Then provide the class in the module:

## Auto Login

If you want to have your app being redirected to the sts automatically without the user clicking any login button you can use the `AutoLoginGuard` provided by the lib. Use it for all the routes you want automatic login to be enabled.
If you want to have your app being redirected to the sts automatically without the user clicking any login button only by accessing a specific you can use the `AutoLoginGuard` provided by the lib. Use it for all the routes you want automatic login to be enabled.

If you are using auto login _make sure_ to _*not*_ call the `checkAuth()` method in your `app.component.ts`. This will be done by the guard automatically for you.
The guard handles `canActivate` and `canLoad` for you.

Sample routes could be
Here are two use cases to distinguish:

1. Redirect route from Security Token Server has a guard in `canLoad` or `canActivate`
2. Redirect route from Token server does _not_ have a guard.

### Redirect route from Token server has a guard

If your redirect route from the Security Token Server to your app has the `AutoLoginGuard` activated already, like this:

```typescript
import { AutoLoginGuard } from 'angular-auth-oidc-client';

const appRoutes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'home' },
{ path: 'home', component: HomeComponent, canActivate: [AutoLoginGuard] },
{ path: 'home', component: HomeComponent, canActivate: [AutoLoginGuard] }, <<<< Redirect Route from STS has the guard
{...
];
```

Then _make sure_ to _*not*_ call the `checkAuth()` method in your `app.component.ts`. This will be done by the guard automatically for you.

### Redirect route from the Token server is public / Does not have a guard

If the redirect route from the STS is publicly available, you _have to_ call the `checkAuth()` by yourself in the `app.component.ts` to proceed the url when getting redirected. The lib redirects you to the route the user entered before he was sent to the login page on the sts automatically for you.

```typescript
import { AutoLoginGuard } from 'angular-auth-oidc-client';

const appRoutes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'home' },
{ path: 'home', component: HomeComponent },
{ path: 'protected', component: ProtectedComponent, canActivate: [AutoLoginGuard] },
{ path: 'forbidden', component: ForbiddenComponent, canActivate: [AutoLoginGuard] },
{ path: 'unauthorized', component: UnauthorizedComponent },
];
```

```ts
export class AppComponent implements OnInit {
constructor(public oidcSecurityService: OidcSecurityService) {}

ngOnInit() {
this.oidcSecurityService.checkAuth().subscribe((isAuthenticated) => {
console.log('app authenticated', isAuthenticated);
const at = this.oidcSecurityService.getToken();
console.log(`Current access token is '${at}'`);
});
}
}
```

[src code](../projects/sample-code-flow-auto-login)

## Custom parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
export * from './auth.module';
export * from './authState/authorization-result';
export * from './authState/authorized-state';
export * from './auto-login/auto-login.guard';
export * from './config/auth-well-known-endpoints';
export * from './config/config.service';
export * from './config/openid-configuration';
export * from './config/public-configuration';
export * from './guards/auto-login.guard';
export * from './interceptor/auth.interceptor';
export * from './logging/log-level';
export * from './logging/logger.service';
Expand Down
2 changes: 2 additions & 0 deletions projects/angular-auth-oidc-client/src/lib/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { NgModule } from '@angular/core';
import { DataService } from './api/data.service';
import { HttpBaseService } from './api/http-base.service';
import { AuthStateService } from './authState/auth-state.service';
import { AutoLoginService } from './auto-login/auto-login-service';
import { ImplicitFlowCallbackService } from './callback/implicit-flow-callback.service';
import { CheckAuthService } from './check-auth.service';
import { ConfigValidationService } from './config-validation/config-validation.service';
Expand Down Expand Up @@ -102,6 +103,7 @@ export class AuthModule {
ParLoginService,
PopUpLoginService,
StandardLoginService,
AutoLoginService,
{
provide: AbstractSecurityStorage,
useClass: token.storage || BrowserStorageService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';

const STORAGE_KEY = 'redirect';

@Injectable()
export class AutoLoginService {
getStoredRedirectRoute() {
return localStorage.getItem(STORAGE_KEY);
}

saveStoredRedirectRoute(url: string) {
localStorage.setItem(STORAGE_KEY, url);
}

deleteStoredRedirectRoute() {
localStorage.removeItem(STORAGE_KEY);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { TestBed, waitForAsync } from '@angular/core/testing';
import { Router, RouterStateSnapshot } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { AuthStateService } from '../authState/auth-state.service';
import { AuthStateServiceMock } from '../authState/auth-state.service-mock';
import { CheckAuthService } from '../check-auth.service';
import { CheckAuthServiceMock } from '../check-auth.service-mock';
import { LoginService } from '../login/login.service';
import { LoginServiceMock } from '../login/login.service-mock';
import { AutoLoginService } from './auto-login-service';
import { AutoLoginGuard } from './auto-login.guard';

describe(`AutoLoginGuard`, () => {
let autoLoginGuard: AutoLoginGuard;
let checkAuthService: CheckAuthService;
let loginService: LoginService;
let authStateService: AuthStateService;
let router: Router;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes([])],
providers: [
AutoLoginService,
{ provide: AuthStateService, useClass: AuthStateServiceMock },
{
provide: LoginService,
useClass: LoginServiceMock,
},
{
provide: CheckAuthService,
useClass: CheckAuthServiceMock,
},
],
});
});

beforeEach(() => {
autoLoginGuard = TestBed.inject(AutoLoginGuard);
checkAuthService = TestBed.inject(CheckAuthService);
authStateService = TestBed.inject(AuthStateService);
router = TestBed.inject(Router);
loginService = TestBed.inject(LoginService);

localStorage.clear();
});

it('should create', () => {
expect(autoLoginGuard).toBeTruthy();
});

describe('canActivate', () => {
it(
'should call checkAuth() if not authenticated already',
waitForAsync(() => {
const checkAuthServiceSpy = spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));

autoLoginGuard.canActivate(null, { url: 'some-url1' } as RouterStateSnapshot).subscribe(() => {
expect(checkAuthServiceSpy).toHaveBeenCalledTimes(1);
});
})
);

it(
'should NOT call checkAuth() if authenticated already',
waitForAsync(() => {
const checkAuthServiceSpy = spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
spyOnProperty(authStateService, 'authorized$', 'get').and.returnValue(of(true));

autoLoginGuard.canActivate(null, { url: 'some-url2' } as RouterStateSnapshot).subscribe(() => {
expect(checkAuthServiceSpy).not.toHaveBeenCalled();
});
})
);

it(
'should call loginService.login() when not authorized',
waitForAsync(() => {
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
const loginSpy = spyOn(loginService, 'login');

autoLoginGuard.canActivate(null, { url: 'some-url3' } as RouterStateSnapshot).subscribe(() => {
expect(loginSpy).toHaveBeenCalledTimes(1);
});
})
);

it(
'should return false when not authorized',
waitForAsync(() => {
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));

autoLoginGuard.canActivate(null, { url: 'some-url4' } as RouterStateSnapshot).subscribe((result) => {
expect(result).toBe(false);
});
})
);

it(
'if no route is stored, setItem on localStorage is called',
waitForAsync(() => {
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
const localStorageSpy = spyOn(localStorage, 'setItem');

autoLoginGuard.canActivate(null, { url: 'some-url5' } as RouterStateSnapshot).subscribe((result) => {
expect(localStorageSpy).toHaveBeenCalledOnceWith('redirect', 'some-url5');
});
})
);

it(
'returns true if authorized',
waitForAsync(() => {
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(true));
const localStorageSpy = spyOn(localStorage, 'setItem');

autoLoginGuard.canActivate(null, { url: 'some-url6' } as RouterStateSnapshot).subscribe((result) => {
expect(result).toBe(true);
expect(localStorageSpy).not.toHaveBeenCalled();
});
})
);

it(
'if authorized and stored route exists: remove item, navigate to route and return true',
waitForAsync(() => {
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(true));
spyOn(localStorage, 'getItem').and.returnValue('stored-route');
const localStorageSpy = spyOn(localStorage, 'removeItem');
const routerSpy = spyOn(router, 'navigate');
const loginSpy = spyOn(loginService, 'login');

autoLoginGuard.canActivate(null, { url: 'some-url7' } as RouterStateSnapshot).subscribe((result) => {
expect(result).toBe(true);
expect(localStorageSpy).toHaveBeenCalledOnceWith('redirect');
expect(routerSpy).toHaveBeenCalledOnceWith(['stored-route']);
expect(loginSpy).not.toHaveBeenCalled();
});
})
);
});

describe('canLoad', () => {
it(
'should call checkAuth() if not authenticated already',
waitForAsync(() => {
const checkAuthServiceSpy = spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));

autoLoginGuard.canLoad({ path: 'some-url8' }, []).subscribe(() => {
expect(checkAuthServiceSpy).toHaveBeenCalledTimes(1);
});
})
);

it(
'should NOT call checkAuth() if authenticated already',
waitForAsync(() => {
const checkAuthServiceSpy = spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
spyOnProperty(authStateService, 'authorized$', 'get').and.returnValue(of(true));

autoLoginGuard.canLoad({ path: 'some-url9' }, []).subscribe(() => {
expect(checkAuthServiceSpy).not.toHaveBeenCalled();
});
})
);

it(
'should call loginService.login() when not authorized',
waitForAsync(() => {
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
const loginSpy = spyOn(loginService, 'login');

autoLoginGuard.canLoad({ path: 'some-url10' }, []).subscribe(() => {
expect(loginSpy).toHaveBeenCalledTimes(1);
});
})
);

it(
'should return false when not authorized',
waitForAsync(() => {
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));

autoLoginGuard.canLoad({ path: 'some-url11' }, []).subscribe((result) => {
expect(result).toBe(false);
});
})
);

it(
'if no route is stored, setItem on localStorage is called',
waitForAsync(() => {
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
const localStorageSpy = spyOn(localStorage, 'setItem');

autoLoginGuard.canLoad({ path: 'some-url12' }, []).subscribe((result) => {
expect(localStorageSpy).toHaveBeenCalledOnceWith('redirect', 'some-url12');
});
})
);

it(
'returns true if authorized',
waitForAsync(() => {
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(true));
const localStorageSpy = spyOn(localStorage, 'setItem');

autoLoginGuard.canLoad({ path: 'some-url13' }, []).subscribe((result) => {
expect(result).toBe(true);
expect(localStorageSpy).not.toHaveBeenCalled();
});
})
);

it(
'if authorized and stored route exists: remove item, navigate to route and return true',
waitForAsync(() => {
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(true));
spyOn(localStorage, 'getItem').and.returnValue('stored-route');
const localStorageSpy = spyOn(localStorage, 'removeItem');
const routerSpy = spyOn(router, 'navigate');
const loginSpy = spyOn(loginService, 'login');

autoLoginGuard.canLoad({ path: 'some-url14' }, []).subscribe((result) => {
expect(result).toBe(true);
expect(localStorageSpy).toHaveBeenCalledOnceWith('redirect');
expect(routerSpy).toHaveBeenCalledOnceWith(['stored-route']);
expect(loginSpy).not.toHaveBeenCalled();
});
})
);
});
});
Loading