-
Notifications
You must be signed in to change notification settings - Fork 113
/
Copy pathauth.service.ts
180 lines (153 loc) · 6.8 KB
/
auth.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { OAuthErrorEvent, OAuthService } from 'angular-oauth2-oidc';
import { BehaviorSubject, combineLatest, Observable, ReplaySubject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class AuthService {
private isAuthenticatedSubject$ = new BehaviorSubject<boolean>(false);
public isAuthenticated$ = this.isAuthenticatedSubject$.asObservable();
private isDoneLoadingSubject$ = new ReplaySubject<boolean>();
public isDoneLoading$ = this.isDoneLoadingSubject$.asObservable();
/**
* Publishes `true` if and only if (a) all the asynchronous initial
* login calls have completed or errorred, and (b) the user ended up
* being authenticated.
*
* In essence, it combines:
*
* - the latest known state of whether the user is authorized
* - whether the ajax calls for initial log in have all been done
*/
public canActivateProtectedRoutes$: Observable<boolean> = combineLatest([
this.isAuthenticated$,
this.isDoneLoading$
]).pipe(map(values => values.every(b => b)));
private navigateToLoginPage() {
// TODO: Remember current URL
this.router.navigateByUrl('/should-login');
}
constructor (
private oauthService: OAuthService,
private router: Router,
) {
// Useful for debugging:
this.oauthService.events.subscribe(event => {
if (event instanceof OAuthErrorEvent) {
console.error(event);
} else {
console.warn(event);
}
});
// This is tricky, as it might cause race conditions (where access_token is set in another
// tab before everything is said and done there.
// TODO: Improve this setup.
window.addEventListener('storage', (event) => {
// The `key` is `null` if the event was caused by `.clear()`
if (event.key !== 'access_token' && event.key !== null) {
return;
}
console.warn('Noticed changes to access_token (most likely from another tab), updating isAuthenticated');
this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken());
if (!this.oauthService.hasValidAccessToken()) {
this.navigateToLoginPage();
}
});
this.oauthService.events
.subscribe(_ => {
this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken());
});
this.oauthService.events
.pipe(filter(e => ['token_received'].includes(e.type)))
.subscribe(e => this.oauthService.loadUserProfile());
this.oauthService.events
.pipe(filter(e => ['session_terminated', 'session_error'].includes(e.type)))
.subscribe(e => this.navigateToLoginPage());
this.oauthService.setupAutomaticSilentRefresh();
}
public runInitialLoginSequence(): Promise<void> {
if (location.hash) {
console.log('Encountered hash fragment, plotting as table...');
console.table(location.hash.substr(1).split('&').map(kvp => kvp.split('=')));
}
// 0. LOAD CONFIG:
// First we have to check to see how the IdServer is
// currently configured:
return this.oauthService.loadDiscoveryDocument()
// For demo purposes, we pretend the previous call was very slow
.then(() => new Promise(resolve => setTimeout(() => resolve(), 1000)))
// 1. HASH LOGIN:
// Try to log in via hash fragment after redirect back
// from IdServer from initImplicitFlow:
.then(() => this.oauthService.tryLogin())
.then(() => {
if (this.oauthService.hasValidAccessToken()) {
return Promise.resolve();
}
// 2. SILENT LOGIN:
// Try to log in via a refresh because then we can prevent
// needing to redirect the user:
return this.oauthService.silentRefresh()
.then(() => Promise.resolve())
.catch(result => {
// Subset of situations from https://openid.net/specs/openid-connect-core-1_0.html#AuthError
// Only the ones where it's reasonably sure that sending the
// user to the IdServer will help.
const errorResponsesRequiringUserInteraction = [
'interaction_required',
'login_required',
'account_selection_required',
'consent_required',
];
if (result
&& result.reason
&& errorResponsesRequiringUserInteraction.indexOf(result.reason.error) >= 0) {
// 3. ASK FOR LOGIN:
// At this point we know for sure that we have to ask the
// user to log in, so we redirect them to the IdServer to
// enter credentials.
//
// Enable this to ALWAYS force a user to login.
// this.oauthService.initImplicitFlow();
//
// Instead, we'll now do this:
console.warn('User interaction is needed to log in, we will wait for the user to manually log in.');
return Promise.resolve();
}
// We can't handle the truth, just pass on the problem to the
// next handler.
return Promise.reject(result);
});
})
.then(() => {
this.isDoneLoadingSubject$.next(true);
// Check for the strings 'undefined' and 'null' just to be sure. Our current
// login(...) should never have this, but in case someone ever calls
// initImplicitFlow(undefined | null) this could happen.
if (this.oauthService.state && this.oauthService.state !== 'undefined' && this.oauthService.state !== 'null') {
let stateUrl = this.oauthService.state;
if (stateUrl.startsWith('/') === false) {
stateUrl = decodeURIComponent(stateUrl);
}
console.log(`There was state of ${this.oauthService.state}, so we are sending you to: ${stateUrl}`);
this.router.navigateByUrl(stateUrl);
}
})
.catch(() => this.isDoneLoadingSubject$.next(true));
}
public login(targetUrl?: string) {
// Note: before version 9.1.0 of the library you needed to
// call encodeURIComponent on the argument to the method.
this.oauthService.initLoginFlow(targetUrl || this.router.url);
}
public logout() { this.oauthService.logOut(); }
public refresh() { this.oauthService.silentRefresh(); }
public hasValidToken() { return this.oauthService.hasValidAccessToken(); }
// These normally won't be exposed from a service like this, but
// for debugging it makes sense.
public get accessToken() { return this.oauthService.getAccessToken(); }
public get refreshToken() { return this.oauthService.getRefreshToken(); }
public get identityClaims() { return this.oauthService.getIdentityClaims(); }
public get idToken() { return this.oauthService.getIdToken(); }
public get logoutUrl() { return this.oauthService.logoutUrl; }
}