Skip to content

Commit 1e27104

Browse files
authored
Merge pull request #1015 from damienbod/fixing-autologin-on-not-guarded-route
Fixing autologin on not guarded route
2 parents aa4853b + 17a2072 commit 1e27104

30 files changed

+525
-201
lines changed

CHANGELOG.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
## Angular Lib for OpenID Connect/OAuth2 Changelog
22

3+
### 2021-03-14 Version 11.6.4
4+
5+
- Improve AutoLoginGuard
6+
- [PR](https://github.com/damienbod/angular-auth-oidc-client/pull/1015)
7+
38
### 2021-03-12 Version 11.6.3
49

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

1015
### 2021-03-05 Version 11.6.2
1116

docs/features.md

+41-4
Original file line numberDiff line numberDiff line change
@@ -91,24 +91,61 @@ Then provide the class in the module:
9191

9292
## Auto Login
9393

94-
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.
94+
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.
9595

96-
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.
96+
The guard handles `canActivate` and `canLoad` for you.
9797

98-
Sample routes could be
98+
Here are two use cases to distinguish:
99+
100+
1. Redirect route from Security Token Server has a guard in `canLoad` or `canActivate`
101+
2. Redirect route from Token server does _not_ have a guard.
102+
103+
### Redirect route from Token server has a guard
104+
105+
If your redirect route from the Security Token Server to your app has the `AutoLoginGuard` activated already, like this:
99106

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

103110
const appRoutes: Routes = [
104111
{ path: '', pathMatch: 'full', redirectTo: 'home' },
105-
{ path: 'home', component: HomeComponent, canActivate: [AutoLoginGuard] },
112+
{ path: 'home', component: HomeComponent, canActivate: [AutoLoginGuard] }, <<<< Redirect Route from STS has the guard
113+
{...
114+
];
115+
```
116+
117+
Then _make sure_ to _*not*_ call the `checkAuth()` method in your `app.component.ts`. This will be done by the guard automatically for you.
118+
119+
### Redirect route from the Token server is public / Does not have a guard
120+
121+
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.
122+
123+
```typescript
124+
import { AutoLoginGuard } from 'angular-auth-oidc-client';
125+
126+
const appRoutes: Routes = [
127+
{ path: '', pathMatch: 'full', redirectTo: 'home' },
128+
{ path: 'home', component: HomeComponent },
106129
{ path: 'protected', component: ProtectedComponent, canActivate: [AutoLoginGuard] },
107130
{ path: 'forbidden', component: ForbiddenComponent, canActivate: [AutoLoginGuard] },
108131
{ path: 'unauthorized', component: UnauthorizedComponent },
109132
];
110133
```
111134

135+
```ts
136+
export class AppComponent implements OnInit {
137+
constructor(public oidcSecurityService: OidcSecurityService) {}
138+
139+
ngOnInit() {
140+
this.oidcSecurityService.checkAuth().subscribe((isAuthenticated) => {
141+
console.log('app authenticated', isAuthenticated);
142+
const at = this.oidcSecurityService.getToken();
143+
console.log(`Current access token is '${at}'`);
144+
});
145+
}
146+
}
147+
```
148+
112149
[src code](../projects/sample-code-flow-auto-login)
113150

114151
## Custom parameters

projects/angular-auth-oidc-client/src/lib/angular-auth-oidc-client.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
export * from './auth.module';
44
export * from './authState/authorization-result';
55
export * from './authState/authorized-state';
6+
export * from './auto-login/auto-login.guard';
67
export * from './config/auth-well-known-endpoints';
78
export * from './config/config.service';
89
export * from './config/openid-configuration';
910
export * from './config/public-configuration';
10-
export * from './guards/auto-login.guard';
1111
export * from './interceptor/auth.interceptor';
1212
export * from './logging/log-level';
1313
export * from './logging/logger.service';

projects/angular-auth-oidc-client/src/lib/auth.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { NgModule } from '@angular/core';
44
import { DataService } from './api/data.service';
55
import { HttpBaseService } from './api/http-base.service';
66
import { AuthStateService } from './authState/auth-state.service';
7+
import { AutoLoginService } from './auto-login/auto-login-service';
78
import { ImplicitFlowCallbackService } from './callback/implicit-flow-callback.service';
89
import { CheckAuthService } from './check-auth.service';
910
import { ConfigValidationService } from './config-validation/config-validation.service';
@@ -102,6 +103,7 @@ export class AuthModule {
102103
ParLoginService,
103104
PopUpLoginService,
104105
StandardLoginService,
106+
AutoLoginService,
105107
{
106108
provide: AbstractSecurityStorage,
107109
useClass: token.storage || BrowserStorageService,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Injectable } from '@angular/core';
2+
3+
const STORAGE_KEY = 'redirect';
4+
5+
@Injectable()
6+
export class AutoLoginService {
7+
getStoredRedirectRoute() {
8+
return localStorage.getItem(STORAGE_KEY);
9+
}
10+
11+
saveStoredRedirectRoute(url: string) {
12+
localStorage.setItem(STORAGE_KEY, url);
13+
}
14+
15+
deleteStoredRedirectRoute() {
16+
localStorage.removeItem(STORAGE_KEY);
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { TestBed, waitForAsync } from '@angular/core/testing';
2+
import { Router, RouterStateSnapshot } from '@angular/router';
3+
import { RouterTestingModule } from '@angular/router/testing';
4+
import { of } from 'rxjs';
5+
import { AuthStateService } from '../authState/auth-state.service';
6+
import { AuthStateServiceMock } from '../authState/auth-state.service-mock';
7+
import { CheckAuthService } from '../check-auth.service';
8+
import { CheckAuthServiceMock } from '../check-auth.service-mock';
9+
import { LoginService } from '../login/login.service';
10+
import { LoginServiceMock } from '../login/login.service-mock';
11+
import { AutoLoginService } from './auto-login-service';
12+
import { AutoLoginGuard } from './auto-login.guard';
13+
14+
describe(`AutoLoginGuard`, () => {
15+
let autoLoginGuard: AutoLoginGuard;
16+
let checkAuthService: CheckAuthService;
17+
let loginService: LoginService;
18+
let authStateService: AuthStateService;
19+
let router: Router;
20+
21+
beforeEach(() => {
22+
TestBed.configureTestingModule({
23+
imports: [RouterTestingModule.withRoutes([])],
24+
providers: [
25+
AutoLoginService,
26+
{ provide: AuthStateService, useClass: AuthStateServiceMock },
27+
{
28+
provide: LoginService,
29+
useClass: LoginServiceMock,
30+
},
31+
{
32+
provide: CheckAuthService,
33+
useClass: CheckAuthServiceMock,
34+
},
35+
],
36+
});
37+
});
38+
39+
beforeEach(() => {
40+
autoLoginGuard = TestBed.inject(AutoLoginGuard);
41+
checkAuthService = TestBed.inject(CheckAuthService);
42+
authStateService = TestBed.inject(AuthStateService);
43+
router = TestBed.inject(Router);
44+
loginService = TestBed.inject(LoginService);
45+
46+
localStorage.clear();
47+
});
48+
49+
it('should create', () => {
50+
expect(autoLoginGuard).toBeTruthy();
51+
});
52+
53+
describe('canActivate', () => {
54+
it(
55+
'should call checkAuth() if not authenticated already',
56+
waitForAsync(() => {
57+
const checkAuthServiceSpy = spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
58+
59+
autoLoginGuard.canActivate(null, { url: 'some-url1' } as RouterStateSnapshot).subscribe(() => {
60+
expect(checkAuthServiceSpy).toHaveBeenCalledTimes(1);
61+
});
62+
})
63+
);
64+
65+
it(
66+
'should NOT call checkAuth() if authenticated already',
67+
waitForAsync(() => {
68+
const checkAuthServiceSpy = spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
69+
spyOnProperty(authStateService, 'authorized$', 'get').and.returnValue(of(true));
70+
71+
autoLoginGuard.canActivate(null, { url: 'some-url2' } as RouterStateSnapshot).subscribe(() => {
72+
expect(checkAuthServiceSpy).not.toHaveBeenCalled();
73+
});
74+
})
75+
);
76+
77+
it(
78+
'should call loginService.login() when not authorized',
79+
waitForAsync(() => {
80+
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
81+
const loginSpy = spyOn(loginService, 'login');
82+
83+
autoLoginGuard.canActivate(null, { url: 'some-url3' } as RouterStateSnapshot).subscribe(() => {
84+
expect(loginSpy).toHaveBeenCalledTimes(1);
85+
});
86+
})
87+
);
88+
89+
it(
90+
'should return false when not authorized',
91+
waitForAsync(() => {
92+
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
93+
94+
autoLoginGuard.canActivate(null, { url: 'some-url4' } as RouterStateSnapshot).subscribe((result) => {
95+
expect(result).toBe(false);
96+
});
97+
})
98+
);
99+
100+
it(
101+
'if no route is stored, setItem on localStorage is called',
102+
waitForAsync(() => {
103+
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
104+
const localStorageSpy = spyOn(localStorage, 'setItem');
105+
106+
autoLoginGuard.canActivate(null, { url: 'some-url5' } as RouterStateSnapshot).subscribe((result) => {
107+
expect(localStorageSpy).toHaveBeenCalledOnceWith('redirect', 'some-url5');
108+
});
109+
})
110+
);
111+
112+
it(
113+
'returns true if authorized',
114+
waitForAsync(() => {
115+
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(true));
116+
const localStorageSpy = spyOn(localStorage, 'setItem');
117+
118+
autoLoginGuard.canActivate(null, { url: 'some-url6' } as RouterStateSnapshot).subscribe((result) => {
119+
expect(result).toBe(true);
120+
expect(localStorageSpy).not.toHaveBeenCalled();
121+
});
122+
})
123+
);
124+
125+
it(
126+
'if authorized and stored route exists: remove item, navigate to route and return true',
127+
waitForAsync(() => {
128+
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(true));
129+
spyOn(localStorage, 'getItem').and.returnValue('stored-route');
130+
const localStorageSpy = spyOn(localStorage, 'removeItem');
131+
const routerSpy = spyOn(router, 'navigate');
132+
const loginSpy = spyOn(loginService, 'login');
133+
134+
autoLoginGuard.canActivate(null, { url: 'some-url7' } as RouterStateSnapshot).subscribe((result) => {
135+
expect(result).toBe(true);
136+
expect(localStorageSpy).toHaveBeenCalledOnceWith('redirect');
137+
expect(routerSpy).toHaveBeenCalledOnceWith(['stored-route']);
138+
expect(loginSpy).not.toHaveBeenCalled();
139+
});
140+
})
141+
);
142+
});
143+
144+
describe('canLoad', () => {
145+
it(
146+
'should call checkAuth() if not authenticated already',
147+
waitForAsync(() => {
148+
const checkAuthServiceSpy = spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
149+
150+
autoLoginGuard.canLoad({ path: 'some-url8' }, []).subscribe(() => {
151+
expect(checkAuthServiceSpy).toHaveBeenCalledTimes(1);
152+
});
153+
})
154+
);
155+
156+
it(
157+
'should NOT call checkAuth() if authenticated already',
158+
waitForAsync(() => {
159+
const checkAuthServiceSpy = spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
160+
spyOnProperty(authStateService, 'authorized$', 'get').and.returnValue(of(true));
161+
162+
autoLoginGuard.canLoad({ path: 'some-url9' }, []).subscribe(() => {
163+
expect(checkAuthServiceSpy).not.toHaveBeenCalled();
164+
});
165+
})
166+
);
167+
168+
it(
169+
'should call loginService.login() when not authorized',
170+
waitForAsync(() => {
171+
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
172+
const loginSpy = spyOn(loginService, 'login');
173+
174+
autoLoginGuard.canLoad({ path: 'some-url10' }, []).subscribe(() => {
175+
expect(loginSpy).toHaveBeenCalledTimes(1);
176+
});
177+
})
178+
);
179+
180+
it(
181+
'should return false when not authorized',
182+
waitForAsync(() => {
183+
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
184+
185+
autoLoginGuard.canLoad({ path: 'some-url11' }, []).subscribe((result) => {
186+
expect(result).toBe(false);
187+
});
188+
})
189+
);
190+
191+
it(
192+
'if no route is stored, setItem on localStorage is called',
193+
waitForAsync(() => {
194+
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(null));
195+
const localStorageSpy = spyOn(localStorage, 'setItem');
196+
197+
autoLoginGuard.canLoad({ path: 'some-url12' }, []).subscribe((result) => {
198+
expect(localStorageSpy).toHaveBeenCalledOnceWith('redirect', 'some-url12');
199+
});
200+
})
201+
);
202+
203+
it(
204+
'returns true if authorized',
205+
waitForAsync(() => {
206+
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(true));
207+
const localStorageSpy = spyOn(localStorage, 'setItem');
208+
209+
autoLoginGuard.canLoad({ path: 'some-url13' }, []).subscribe((result) => {
210+
expect(result).toBe(true);
211+
expect(localStorageSpy).not.toHaveBeenCalled();
212+
});
213+
})
214+
);
215+
216+
it(
217+
'if authorized and stored route exists: remove item, navigate to route and return true',
218+
waitForAsync(() => {
219+
spyOn(checkAuthService, 'checkAuth').and.returnValue(of(true));
220+
spyOn(localStorage, 'getItem').and.returnValue('stored-route');
221+
const localStorageSpy = spyOn(localStorage, 'removeItem');
222+
const routerSpy = spyOn(router, 'navigate');
223+
const loginSpy = spyOn(loginService, 'login');
224+
225+
autoLoginGuard.canLoad({ path: 'some-url14' }, []).subscribe((result) => {
226+
expect(result).toBe(true);
227+
expect(localStorageSpy).toHaveBeenCalledOnceWith('redirect');
228+
expect(routerSpy).toHaveBeenCalledOnceWith(['stored-route']);
229+
expect(loginSpy).not.toHaveBeenCalled();
230+
});
231+
})
232+
);
233+
});
234+
});

0 commit comments

Comments
 (0)