UNPKG

17.8 kBMarkdownView Raw
1# angular-oauth2-oidc
2
3Support for OAuth 2 and OpenId Connect (OIDC) in Angular.
4
5![OIDC Certified Logo](https://raw.githubusercontent.com/manfredsteyer/angular-oauth2-oidc/master/oidc.png)
6
7## Credits
8
9- [generator-angular2-library](https://github.com/jvandemo/generator-angular2-library) for scaffolding a angular library
10- [jsrasign](https://kjur.github.io/jsrsasign/) for validating token signature and for hashing
11- [Identity Server](https://github.com/identityserver) (used for Testing with an .NET/.NET Core Backend)
12- [Keycloak (Redhad)](http://www.keycloak.org/) for Testing with Java
13
14## Resources
15
16- Sources and Sample:
17https://github.com/manfredsteyer/angular-oauth2-oidc
18
19- Source Code Documentation
20https://manfredsteyer.github.io/angular-oauth2-oidc/angular-oauth2-oidc/docs/
21
22## Tested Environment
23
24Successfully tested with the Angular 2 and 4 and its Router, PathLocationStrategy as well as HashLocationStrategy and CommonJS-Bundling via webpack. At server side we've used IdentityServer (.NET/ .NET Core) and Redhat's Keycloak (Java).
25
26## New Features in Version 2
27- Token Refresh for Implicit Flow by implementing "silent refresh"
28- Validating the signature of the received id_token
29- Providing Events via the observable ``events``.
30- The event ``token_expires`` can be used togehter with a silent refresh to automatically refresh a token when/ before it expires (see also property ``timeoutFactor``).
31
32## Additional Features
33
34- Logging in via OAuth2 and OpenId Connect (OIDC) Implicit Flow (where user is redirected to Identity Provider)
35- "Logging in" via Password Flow (where user enters his/her password into the client)
36- Token Refresh for Password Flow by using a Refresh Token
37- Automatically refreshing a token when/ some time before it expires
38- Querying Userinfo Endpoint
39- Querying Discovery Document to ease configuration
40- Validating claims of the id_token regarding the specs
41- Hook for further custom validations
42- Single-Sign-Out by redirecting to the auth-server's logout-endpoint
43
44## Breaking Changes in Version 2
45
46- The property ``oidc`` defaults to ``true``.
47- If you are just using oauth2, you have to set ``oidc`` to ``false``. Otherwise, the validation of the user profile will fail!
48- By default, ``sessionStorage`` is used. To use ``localStorage`` call method setStorage
49- Demands using https as OIDC and OAuth2 relay on it. This rule can be relaxed using the property ``requireHttps``, e. g. for local testing.
50- Demands that every url provided by the discovery document starts with the issuer's url. This can be relaxed by using the property ``strictDiscoveryDocumentValidation``.
51
52## Sample-Auth-Server
53
54You can use the OIDC-Sample-Server mentioned in the samples for Testing. It assumes, that your Web-App runns on http://localhost:8080.
55
56Username/Password: max/geheim
57
58*clientIds:*
59- spa-demo (implicit flow)
60- demo-resource-owner (resource owner password flow)
61
62*redirectUris:*
63- localhost:[8080-8089|4200-4202]
64- localhost:[8080-8089|4200-4202]/index.html
65- localhost:[8080-8089|4200-4202]/silent-refresh.html
66
67
68## Setup Provider for OAuthService
69
70```
71import { OAuthModule } from 'angular-oauth2-oidc';
72[...]
73
74@NgModule({
75 imports: [
76 [...]
77 HttpModule,
78 OAuthModule.forRoot()
79 ],
80 declarations: [
81 AppComponent,
82 HomeComponent,
83 [...]
84 ],
85 bootstrap: [
86 AppComponent
87 ]
88})
89export class AppModule {
90}
91
92```
93
94## Using Implicit Flow
95
96This section shows how to use the implicit flow, which is redirecting the user to the auth-server for the login.
97
98### Configure Library for Implicit Flow (using discovery document)
99
100To configure the library you just have to set some properties on startup. For this, the following sample uses the constructor of the AppComponent which is called before routing kicks in.
101
102```
103@Component({ ... })
104export class AppComponent {
105
106 constructor(private oauthService: OAuthService) {
107
108 // URL of the SPA to redirect the user to after login
109 this.oauthService.redirectUri = window.location.origin + "/index.html";
110
111 // The SPA's id. The SPA is registerd with this id at the auth-server
112 this.oauthService.clientId = "spa-demo";
113
114 // set the scope for the permissions the client should request
115 // The first three are defined by OIDC. The 4th is a usecase-specific one
116 this.oauthService.scope = "openid profile email voucher";
117
118 // The name of the auth-server that has to be mentioned within the token
119 this.oauthService.issuer = "https://steyer-identity-server.azurewebsites.net/identity";
120
121 // Load Discovery Document and then try to login the user
122 this.oauthService.loadDiscoveryDocument().then(() => {
123
124 // This method just tries to parse the token(s) within the url when
125 // the auth-server redirects the user back to the web-app
126 // It dosn't send the user the the login page
127 this.oauthService.tryLogin();
128
129 });
130
131 }
132
133}
134```
135
136### Configure Library for Implicit Flow (without discovery document)
137
138When you don't have a discovery document, you have to configure more properties manually:
139
140```
141@Component({ ... })
142export class AppComponent {
143
144 constructor(private oauthService: OAuthService) {
145
146 // Login-Url
147 this.oauthService.loginUrl = "https://steyer-identity-server.azurewebsites.net/identity/connect/authorize"; //Id-Provider?
148
149 // URL of the SPA to redirect the user to after login
150 this.oauthService.redirectUri = window.location.origin + "/index.html";
151
152 // The SPA's id. Register SPA with this id at the auth-server
153 this.oauthService.clientId = "spa-demo";
154
155 // set the scope for the permissions the client should request
156 this.oauthService.scope = "openid profile email voucher";
157
158 // Use setStorage to use sessionStorage or another implementation of the TS-type Storage
159 // instead of localStorage
160 this.oauthService.setStorage(sessionStorage);
161
162 // To also enable single-sign-out set the url for your auth-server's logout-endpoint here
163 this.oauthService.logoutUrl = "https://steyer-identity-server.azurewebsites.net/identity/connect/endsession";
164
165 // This method just tries to parse the token(s) within the url when
166 // the auth-server redirects the user back to the web-app
167 // It dosn't send the user the the login page
168 this.oauthService.tryLogin();
169
170
171 }
172
173}
174```
175
176### Home-Component (for login)
177
178```
179import { Component } from '@angular/core';
180import { OAuthService } from 'angular-oauth2-oidc';
181
182@Component({
183 templateUrl: "app/home.html"
184})
185export class HomeComponent {
186
187 constructor(private oAuthService: OAuthService) {
188 }
189
190 public login() {
191 this.oAuthService.initImplicitFlow();
192 }
193
194 public logoff() {
195 this.oAuthService.logOut();
196 }
197
198 public get name() {
199 let claims = this.oAuthService.getIdentityClaims();
200 if (!claims) return null;
201 return claims.given_name;
202 }
203
204}
205```
206
207```
208<h1 *ngIf="!name">
209 Hallo
210</h1>
211<h1 *ngIf="name">
212 Hallo, {{name}}
213</h1>
214
215<button class="btn btn-default" (click)="login()">
216 Login
217</button>
218<button class="btn btn-default" (click)="logoff()">
219 Logout
220</button>
221
222<div>
223 Username/Passwort zum Testen: max/geheim
224</div>
225```
226
227### Validate id_token
228
229You can hook in an implementation of the interface ``TokenValidator`` to validate the signature of the received id_token and its at_hash property. This packages provides two implementations:
230
231- JwksValidationHandler
232- NullValidationHandler
233
234The former one validates the signature against public keys received via the discovery document (property jwks) and the later one skips the validation on client side.
235
236```
237import { JwksValidationHandler } from 'angular-oauth2-oidc';
238
239[...]
240
241this.oauthService.tokenValidationHandler = new JwksValidationHandler();
242```
243
244In cases where no ValidationHandler is defined, you receive a warning on the console. This means that the library wants you to explicitly decide on this.
245
246### Calling a Web API with OAuth-Token
247
248Pass this Header to the used method of the ``Http``-Service within an Instance of the class ``Headers``:
249
250```
251var headers = new Headers({
252 "Authorization": "Bearer " + this.oauthService.getAccessToken()
253});
254```
255
256### Refreshing a Token when using Implicit Flow
257
258To refresh your tokens when using implicit flow you can use a silent refresh. This is a well-known solution that compensates the fact that implicit flow does not allow for issuing a refresh token. It uses a hidden iframe to get another token from the auth-server. When the user is there still logged in (by using a cookie) it will respond without user interaction and provide new tokens.
259
260To use this approach, setup a redirect uri for the silent refresh:
261
262```
263this.oauthService.silentRefreshRedirectUri = window.location.origin + "/silent-refresh.html";
264```
265
266Please keep in mind that this uri has to be configured at the auth-server too.
267
268This file is loaded into the hidden iframe after getting new tokens. Its only task is to send the received tokens to the main application:
269
270```
271<html>
272<body>
273 <script>
274 parent.postMessage(location.hash, location.origin);
275 </script>
276</body>
277</html>
278```
279
280Please make sure that this file is copied to your output directory by your build task. When using the CLI you can define it as an asset for this. For this, you have to add the following line to the file ``.angular-cli.json``:
281
282```
283"assets": [
284 [...],
285 "silent-refresh.html"
286],
287```
288
289To perform a silent refresh, just call the following method:
290
291```
292this
293 .oauthService
294 .silentRefresh()
295 .then(info => console.debug('refresh ok', info))
296 .catch(err => console.error('refresh error', err));
297```
298
299When there is an error in the iframe that prevents the communication with the main application, silentRefresh will give you a timeout. To configure the timespan for this, you can set the property ``siletRefreshTimeout`` (msec). The default value is 20.000 (20 seconds).
300
301### Automatically refreshing a token when/ before it expires
302
303To automatically refresh a token when/ some time before it expires, you can make use of the event ``token_expires``:
304
305```
306this
307 .oauthService
308 .events
309 .filter(e => e.type == 'token_expires')
310 .subscribe(e => {
311 this.oauthService.silentRefresh();
312 });
313```
314
315By default, this event is fired after 75% of the token's life time is over. You can adjust this factor by setting the property ``timeoutFactor`` to a value between 0 and 1. For instance, 0.5 means, that the event is fired after half of the life time is over and 0.33 triggers the event after a third.
316
317### Callback after successful login
318
319There is a callback ``onTokenReceived``, that is called after a successful login. In this case, the lib received the access_token as
320well as the id_token, if it was requested. If there is an id_token, the lib validated it.
321
322```
323this.oauthService.tryLogin({
324 onTokenReceived: context => {
325 //
326 // Output just for purpose of demonstration
327 // Don't try this at home ... ;-)
328 //
329 console.debug("logged in");
330 console.debug(context);
331 }
332});
333```
334
335## Preserving State like the requested URL
336
337When calling ``initImplicitFlow``, you can pass an optional state which could be the requested url:
338
339```
340this.oauthService.initImplicitFlow('http://www.myurl.com/x/y/z');
341```
342
343After login succeeded, you can read this state:
344
345```
346this.oauthService.tryLogin({
347 onTokenReceived: (info) => {
348 console.debug('state', info.state);
349 }
350})
351```
352
353### Custom Query Parameter
354
355You can set the property ``customQueryParams`` to a hash with custom parameter that are transmitted when starting implicit flow.
356
357```
358this.oauthService.customQueryParams = {
359 'tenant': '4711',
360 'otherParam': 'someValue'
361};
362```
363
364## Routing with the HashStrategy
365
366If you are leveraging the ``LocationStrategy`` which the Router is using by default, you can skip this section.
367
368When using the ``HashStrategy`` for Routing, the Router will override the received hash fragment with the tokens when it performs it initial navigation. This prevents the library from reading them. To avoid this, disable initial navigation when setting up the routes for your root module:
369
370```
371export let AppRouterModule = RouterModule.forRoot(APP_ROUTES, {
372 useHash: true,
373 initialNavigation: false
374});
375```
376
377After tryLogin did its job, you can manually perform the initial navigation:
378
379```
380this.oauthService.tryLogin().then(_ => {
381 this.router.navigate(['/']);
382})
383```
384
385Another solution is the use a redirect uri that already contains the initial route. In this case the router will not override it. An example for such a redirect uri is
386
387```
388 http://localhost:8080/#/home
389```
390
391## Events
392
393```
394this.oauthService.events.subscribe(e => {
395 console.debug('oauth/oidc event', e);
396})
397```
398
399## Using Password-Flow
400
401This section shows how to use the password flow, which demands the user to directly enter his or her password into the client.
402
403### Configure Library for Password Flow (using discovery document)
404
405To configure the library you just have to set some properties on startup. For this, the following sample uses the constructor of the AppComponent which is called before routing kicks in.
406
407Please not, that this configuation is quite similar to the one for the implcit flow.
408
409```
410@Component({ ... })
411export class AppComponent {
412
413 constructor(private oauthService: OAuthService) {
414
415 // The SPA's id. Register SPA with this id at the auth-server
416 this.oauthService.clientId = "demo-resource-owner";
417
418 // set the scope for the permissions the client should request
419 // The auth-server used here only returns a refresh token (see below), when the scope offline_access is requested
420 this.oauthService.scope = "openid profile email voucher offline_access";
421
422 // Use setStorage to use sessionStorage or another implementation of the TS-type Storage
423 // instead of localStorage
424 this.oauthService.setStorage(sessionStorage);
425
426 // Set a dummy secret
427 // Please note that the auth-server used here demand the client to transmit a client secret, although
428 // the standard explicitly cites that the password flow can also be used without it. Using a client secret
429 // does not make sense for a SPA that runs in the browser. That's why the property is called dummyClientSecret
430 // Using such a dummy secreat is as safe as using no secret.
431 this.oauthService.dummyClientSecret = "geheim";
432
433 // Load Discovery Document and then try to login the user
434 let url = 'https://steyer-identity-server.azurewebsites.net/identity/.well-known/openid-configuration';
435 this.oauthService.loadDiscoveryDocument(url).then(() => {
436 // Do what ever you want here
437 });
438
439 }
440
441}
442```
443
444### Configure Library for Password Flow (without discovery document)
445
446In cases where you don't have an OIDC based discovery document you have to configure some more properties manually:
447
448```
449@Component({ ... })
450export class AppComponent {
451
452 constructor(private oauthService: OAuthService) {
453
454 // Login-Url
455 this.oauthService.tokenEndpoint = "https://steyer-identity-server.azurewebsites.net/identity/connect/token";
456
457 // Url with user info endpoint
458 // This endpont is described by OIDC and provides data about the loggin user
459 // This sample uses it, because we don't get an id_token when we use the password flow
460 // If you don't want this lib to fetch data about the user (e. g. id, name, email) you can skip this line
461 this.oauthService.userinfoEndpoint = "https://steyer-identity-server.azurewebsites.net/identity/connect/userinfo";
462
463 // The SPA's id. Register SPA with this id at the auth-server
464 this.oauthService.clientId = "demo-resource-owner";
465
466 // set the scope for the permissions the client should request
467 this.oauthService.scope = "openid profile email voucher offline_access";
468
469 // Set a dummy secret
470 // Please note that the auth-server used here demand the client to transmit a client secret, although
471 // the standard explicitly cites that the password flow can also be used without it. Using a client secret
472 // does not make sense for a SPA that runs in the browser. That's why the property is called dummyClientSecret
473 // Using such a dummy secreat is as safe as using no secret.
474 this.oauthService.dummyClientSecret = "geheim";
475
476 }
477
478}
479```
480
481### Fetching an Access Token by providing the current user's credentials
482
483```
484this.oauthService.fetchTokenUsingPasswordFlow('max', 'geheim').then((resp) => {
485
486 // Loading data about the user
487 return this.oauthService.loadUserProfile();
488
489}).then(() => {
490
491 // Using the loaded user data
492 let claims = this.oAuthService.getIdentityClaims();
493 if (claims) console.debug('given_name', claims.given_name);
494
495})
496```
497
498There is also a short form for fetching the token and loading the user profile:
499
500```
501this.oauthService.fetchTokenUsingPasswordFlowAndLoadUserProfile('max', 'geheim').then(() => {
502 let claims = this.oAuthService.getIdentityClaims();
503 if (claims) console.debug('given_name', claims.given_name);
504});
505```
506
507### Refreshing the current Access Token
508
509Using the password flow you MIGHT get a refresh token (which isn't the case with the implicit flow by design!). You can use this token later to get a new access token, e. g. after it expired.
510
511```
512this.oauthService.refreshToken().then(() => {
513 console.debug('ok');
514})
515```
\No newline at end of file