├── DemoApp ├── src │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── silent-refresh.html │ ├── tsconfig.app.json │ ├── index.html │ ├── tslint.json │ ├── browserslist │ ├── main.ts │ ├── styles.css │ ├── app │ │ ├── app.module.ts │ │ └── app.component.ts │ └── polyfills.ts ├── tsconfig.json ├── README.md ├── package.json ├── tslint.json └── angular.json ├── second-load.png ├── auth0-sign-up.png ├── implicit-flow.png ├── initial-load.png ├── auth0-authorize.png ├── auth0-create-api.png ├── scaffolded-app.png ├── auth0-create-application.png ├── .editorconfig ├── .gitignore └── README.md /DemoApp/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /DemoApp/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /second-load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/sample-auth0-angular-oauth2-oidc/HEAD/second-load.png -------------------------------------------------------------------------------- /auth0-sign-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/sample-auth0-angular-oauth2-oidc/HEAD/auth0-sign-up.png -------------------------------------------------------------------------------- /implicit-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/sample-auth0-angular-oauth2-oidc/HEAD/implicit-flow.png -------------------------------------------------------------------------------- /initial-load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/sample-auth0-angular-oauth2-oidc/HEAD/initial-load.png -------------------------------------------------------------------------------- /auth0-authorize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/sample-auth0-angular-oauth2-oidc/HEAD/auth0-authorize.png -------------------------------------------------------------------------------- /auth0-create-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/sample-auth0-angular-oauth2-oidc/HEAD/auth0-create-api.png -------------------------------------------------------------------------------- /scaffolded-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/sample-auth0-angular-oauth2-oidc/HEAD/scaffolded-app.png -------------------------------------------------------------------------------- /DemoApp/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/sample-auth0-angular-oauth2-oidc/HEAD/DemoApp/src/favicon.ico -------------------------------------------------------------------------------- /auth0-create-application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/sample-auth0-angular-oauth2-oidc/HEAD/auth0-create-application.png -------------------------------------------------------------------------------- /DemoApp/src/silent-refresh.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /DemoApp/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | max_line_length = off 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /DemoApp/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |You are logged in as {{username}}.
8 |9 | 10 | 11 | 12 |
13 |Access Token
{{token | json}}
14 | Claims
{{claims | json}}
15 | `,
16 | styles: []
17 | })
18 | export class AppComponent {
19 | username = '';
20 |
21 | get token() { return this.oauthService.getAccessToken(); }
22 | get claims() { return this.oauthService.getIdentityClaims(); }
23 |
24 | constructor(private oauthService: OAuthService) {
25 | // For debugging:
26 | oauthService.events.subscribe(e => e instanceof OAuthErrorEvent ? console.error(e) : console.warn(e));
27 |
28 | // Load information from Auth0 (could also be configured manually)
29 | oauthService.loadDiscoveryDocument()
30 |
31 | // See if the hash fragment contains tokens (when user got redirected back)
32 | .then(() => oauthService.tryLogin())
33 |
34 | // If we're still not logged in yet, try with a silent refresh:
35 | .then(() => {
36 | if (!oauthService.hasValidAccessToken()) {
37 | return oauthService.silentRefresh();
38 | }
39 | })
40 |
41 | // Get username, if possible.
42 | .then(() => {
43 | if (oauthService.getIdentityClaims()) {
44 | this.username = oauthService.getIdentityClaims()['name'];
45 | }
46 | });
47 |
48 | oauthService.setupAutomaticSilentRefresh();
49 | }
50 |
51 | login() { this.oauthService.initImplicitFlow(); }
52 | logout() { this.oauthService.logOut(); }
53 | refresh() { this.oauthService.silentRefresh(); }
54 | }
55 |
--------------------------------------------------------------------------------
/DemoApp/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "deprecation": {
15 | "severity": "warn"
16 | },
17 | "eofline": true,
18 | "forin": true,
19 | "import-blacklist": [
20 | true,
21 | "rxjs/Rx"
22 | ],
23 | "import-spacing": true,
24 | "indent": [
25 | true,
26 | "spaces"
27 | ],
28 | "interface-over-type-literal": true,
29 | "label-position": true,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-arg": true,
47 | "no-bitwise": true,
48 | "no-console": [
49 | true,
50 | "debug",
51 | "info",
52 | "time",
53 | "timeEnd",
54 | "trace"
55 | ],
56 | "no-construct": true,
57 | "no-debugger": true,
58 | "no-duplicate-super": true,
59 | "no-empty": false,
60 | "no-empty-interface": true,
61 | "no-eval": true,
62 | "no-inferrable-types": [
63 | true,
64 | "ignore-params"
65 | ],
66 | "no-misused-new": true,
67 | "no-non-null-assertion": true,
68 | "no-shadowed-variable": true,
69 | "no-string-literal": false,
70 | "no-string-throw": true,
71 | "no-switch-case-fall-through": true,
72 | "no-trailing-whitespace": true,
73 | "no-unnecessary-initializer": true,
74 | "no-unused-expression": true,
75 | "no-use-before-declare": true,
76 | "no-var-keyword": true,
77 | "object-literal-sort-keys": false,
78 | "one-line": [
79 | true,
80 | "check-open-brace",
81 | "check-catch",
82 | "check-else",
83 | "check-whitespace"
84 | ],
85 | "prefer-const": true,
86 | "quotemark": [
87 | true,
88 | "single"
89 | ],
90 | "radix": true,
91 | "semicolon": [
92 | true,
93 | "always"
94 | ],
95 | "triple-equals": [
96 | true,
97 | "allow-null-check"
98 | ],
99 | "typedef-whitespace": [
100 | true,
101 | {
102 | "call-signature": "nospace",
103 | "index-signature": "nospace",
104 | "parameter": "nospace",
105 | "property-declaration": "nospace",
106 | "variable-declaration": "nospace"
107 | }
108 | ],
109 | "unified-signatures": true,
110 | "variable-name": false,
111 | "whitespace": [
112 | true,
113 | "check-branch",
114 | "check-decl",
115 | "check-operator",
116 | "check-separator",
117 | "check-type"
118 | ],
119 | "no-output-on-prefix": true,
120 | "use-input-property-decorator": true,
121 | "use-output-property-decorator": true,
122 | "use-host-property-decorator": true,
123 | "no-input-rename": true,
124 | "no-output-rename": true,
125 | "use-life-cycle-interface": true,
126 | "use-pipe-transform-interface": true,
127 | "component-class-suffix": true,
128 | "directive-class-suffix": true
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/DemoApp/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** IE10 and IE11 requires the following for the Reflect API. */
41 | // import 'core-js/es6/reflect';
42 |
43 |
44 | /** Evergreen browsers require these. **/
45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
46 | import 'core-js/es7/reflect';
47 |
48 |
49 | /**
50 | * Web Animations `@angular/platform-browser/animations`
51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
53 | **/
54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
55 |
56 | /**
57 | * By default, zone.js will patch all possible macroTask and DomEvents
58 | * user can disable parts of macroTask/DomEvents patch by setting following flags
59 | */
60 |
61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
64 |
65 | /*
66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
68 | */
69 | // (window as any).__Zone_enable_cross_context_check = true;
70 |
71 | /***************************************************************************************************
72 | * Zone JS is required by default for Angular itself.
73 | */
74 | import 'zone.js/dist/zone'; // Included with Angular CLI.
75 |
76 |
77 |
78 | /***************************************************************************************************
79 | * APPLICATION IMPORTS
80 | */
81 |
--------------------------------------------------------------------------------
/DemoApp/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "DemoApp": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "prefix": "app",
11 | "schematics": {
12 | "@schematics/angular:component": {
13 | "inlineTemplate": true,
14 | "inlineStyle": true,
15 | "spec": false
16 | },
17 | "@schematics/angular:class": {
18 | "spec": false
19 | },
20 | "@schematics/angular:directive": {
21 | "spec": false
22 | },
23 | "@schematics/angular:guard": {
24 | "spec": false
25 | },
26 | "@schematics/angular:module": {
27 | "spec": false
28 | },
29 | "@schematics/angular:pipe": {
30 | "spec": false
31 | },
32 | "@schematics/angular:service": {
33 | "spec": false
34 | }
35 | },
36 | "architect": {
37 | "build": {
38 | "builder": "@angular-devkit/build-angular:browser",
39 | "options": {
40 | "outputPath": "dist/DemoApp",
41 | "index": "src/index.html",
42 | "main": "src/main.ts",
43 | "polyfills": "src/polyfills.ts",
44 | "tsConfig": "src/tsconfig.app.json",
45 | "assets": [
46 | "src/silent-refresh.html",
47 | "src/favicon.ico",
48 | "src/assets"
49 | ],
50 | "styles": [
51 | "src/styles.css"
52 | ],
53 | "scripts": []
54 | },
55 | "configurations": {
56 | "production": {
57 | "fileReplacements": [
58 | {
59 | "replace": "src/environments/environment.ts",
60 | "with": "src/environments/environment.prod.ts"
61 | }
62 | ],
63 | "optimization": true,
64 | "outputHashing": "all",
65 | "sourceMap": false,
66 | "extractCss": true,
67 | "namedChunks": false,
68 | "aot": true,
69 | "extractLicenses": true,
70 | "vendorChunk": false,
71 | "buildOptimizer": true
72 | }
73 | }
74 | },
75 | "serve": {
76 | "builder": "@angular-devkit/build-angular:dev-server",
77 | "options": {
78 | "browserTarget": "DemoApp:build"
79 | },
80 | "configurations": {
81 | "production": {
82 | "browserTarget": "DemoApp:build:production"
83 | }
84 | }
85 | },
86 | "extract-i18n": {
87 | "builder": "@angular-devkit/build-angular:extract-i18n",
88 | "options": {
89 | "browserTarget": "DemoApp:build"
90 | }
91 | },
92 | "lint": {
93 | "builder": "@angular-devkit/build-angular:tslint",
94 | "options": {
95 | "tsConfig": [
96 | "src/tsconfig.app.json",
97 | "src/tsconfig.spec.json"
98 | ],
99 | "exclude": [
100 | "**/node_modules/**"
101 | ]
102 | }
103 | }
104 | }
105 | }
106 | },
107 | "defaultProject": "DemoApp"
108 | }
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sample Auth0 and Angular-OAuth2-OIDC Application
2 |
3 | This repository demonstrates how to connect your Angular 6 application to Auth0 using the implicit flow.
4 | It is the companion to a blog post written for [Infi](https://infi.nl).
5 |
6 | ## ⚠️ Notice about updates
7 |
8 | Note that this repository is provided "as-is" and will most likely not receive any (security) updates.
9 |
10 | ## Disclaimers
11 |
12 | Let's get some disclaimers out of the way.
13 | This repository is **frozen in time**: it was created in **September 2018** and probably never updated.
14 | So you might have to adjust the advice here for your own timeline.
15 |
16 | Second, this repository demonstrates **how to connect the dots** but it also **glosses over details**.
17 | It should help you get started, or grasp the idea.
18 | But please adjust accordingly for production applications.
19 |
20 | ## Let's get started
21 |
22 | This is a very specific, technical, pragmatic post.
23 | It's just what we enjoy at Infi: getting those important details just right.
24 | If you came here for fluffy content about agile, or projects, or fun stuff: better skip this post.
25 | You've been warned!
26 |
27 | Actually, this is one post in a series:
28 |
29 | 1. The "About" part, describing all moving parts and processes.
30 | 1. The "Gimme-teh-codez" part, that walks you through the code.
31 |
32 | If you already know how the Implicit Flow works, you can safely skip parts of the post.
33 | If code says more than words to you, or if you know how Auth0 works, you can safely skip the entire post, and go straight to part 2.
34 | For the rest of us, we'll start at the beginning.
35 |
36 | ## About the things involved
37 |
38 | We'll start by getting our terminology straight.
39 | What's what!?
40 |
41 | ### The Implicit Flow
42 |
43 | OAuth2 and OpenID Connect are standards for how to authenticate and (to some degree) authorize users in your systems.
44 | It assumes this type of setup with three items:
45 |
46 | 1. An **Identity Server** application handles user accounts, passwords, 2FA, and all that good jazz.
47 | 1. **Clients** (like an Angular application) that send their users to the Id Server to log in, (after which they're redirected back to the Client).
48 | 1. Your **API**, which needs the access token on each call to verify access.
49 |
50 | This differs from the slightly simpler (but less secure) **Resource Owner Password** flow.
51 | With the Implicit Flow the Client never sees credentials: users trust only the Id Server with those.
52 | On the downside, you do have some redirection going on for the user.
53 | The user sees login screens from the Id Server, but this should not be a big problem because:
54 |
55 | - Either it's a well-known provider, and users are right to trust it.
56 | - Or it's your own identity server, and you can style things to make it "part of the client experience".
57 |
58 | Oh, and this flow also quite naturally supports external Identity Providers (the "log in with Google/GitHub/etc" stuff).
59 | Which is very nice for users.
60 |
61 | Footnote: read more about [the Implicit Flow in RFC 6749](https://tools.ietf.org/html/rfc6749#section-1.3.2).
62 |
63 | ### The Identity Server
64 |
65 | You can of course create your own Identity Server.
66 | Security is hard though, so don't completely roll your own.
67 | Instead, use an existing solution to build from.
68 |
69 | There's good ones available for nearly every tech stack.
70 | For .NET there's [IdentityServer](https://identityserver.io/), Java e.g. has [spring-oauth-server](https://github.com/authlete/spring-oauth-server), and so forth.
71 |
72 | However, there are also SAAS solutions (sometimes called IDaaS) available.
73 | For example [Okta](https://www.okta.com/), [Keycloak](https://www.keycloak.org/), and [Auth0](https://auth0.com/).
74 | In this tutorial we use **Auth0** (a comparison is left for another time).
75 |
76 | ### The API
77 |
78 | In this post we won't touch on the API side of things.
79 | The beauty of OAuth2 is that the API side of things is largely *decoupled* from the rest.
80 | We will get to the point where access tokens are sent to a dummy API, and assume everything would work from there.
81 |
82 | There is one important note about the flow though.
83 | Tokens are passed plainly to the API by the client application.
84 | The format for such tokens is "JWT" (pronounced like "jot"), typically at least *signed* (JWS), or alternatively *encrypted* (JWE).
85 | Your API can verify (or decrypt) the tokens.
86 |
87 | To do this the API will need to get the public key (or decryption key) from the ID Server.
88 | It typically does so "live", by calling the ID Server (cached and refreshed every so often).
89 | But you can also provide these keys out of band.
90 |
91 | Footnote: read more about [JSON Web Tokens (JWT) in RFC 7519](https://tools.ietf.org/html/rfc7519).
92 |
93 | ### The Client
94 |
95 | For OAuth2, a "Client" is an abstract concept.
96 | It can be a Single Page Web App, a mobile application, a traditional MVC Web App, or even another API.
97 | When talking about "the Client" in this post, we're talking about our Angular 6+ CLI application.
98 | The Implicit Flow is well-suited for Single Page (JavaScript) Applications.
99 |
100 | When a Client determines that a user should log in, it redirects the user to the Identity Server.
101 | The user logs in at the Identity Server, and gets redirected back to the Client.
102 | The Client at this point expects that the user "brings back" the access token (and possibly id token).
103 | This is typically done by the Identity Server redirecting back to the URL of the client with tokens in the hash fragment of the URL.
104 |
105 | We will build an Angular CLI application from scratch in part 2 of this series.
106 | In it we will use one of the available libraries for handling the OAuth2 and OpenID Connect parts: [`angular-oauth2-oidc`](https://github.com/manfredsteyer/angular-oauth2-oidc).
107 | The Angular application will require users to log in with Auth0, and send the retrieved tokens along to the dummy API.
108 |
109 | Footnote: read more about [Clients in RFC 6749](https://tools.ietf.org/html/rfc6749#section-2).
110 |
111 | ## Putting things together
112 |
113 | Let's put what we learned above in a picture:
114 |
115 | 
116 |
117 | The two lines between Client and ID Server in this (simplified!) visualization *are* "the Flow".
118 | They determine how a Client can retrieve a token from the ID Server.
119 |
120 | Some specific things missing in the diagram are:
121 |
122 | - All log in screens being served by the ID Server. We use Auth0 in this series, so it's all taken care of for us.
123 | - Third party logins, e.g. "Log in with Google". This would include a fourth box all the way to the left, but with Auth0 that only requires configuration, no coding on our part.
124 | - Silent refreshes: access tokens are short lived, so you need to get a fresh one every hour or so. Turns out that's just the normal flow in a hidden iframe, how it works is shown when we work on the code.
125 |
126 | And that's all there is to it!
127 | In the next part we will start working on the actual code.
128 |
129 | ## Let's Code
130 |
131 | In the previous part we discussed the OAuth2 Implicit Flow.
132 | But code is a more efficient way of communicating, don't you think?
133 | So let's get to it!
134 |
135 | If you're here just for the code: no problem!
136 | To get a full example of how the client side library is supposed to work, you should check out [the `sample-angular-oauth2-oidc-with-auth-guards` repository](https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards), which has a *production-worthy* example of how to use the library.
137 | It would need to be reconfigured to use Auth0 however.
138 | So if you want to see a *minimal* example of how to use it with Auth0, check out [this post's companion repository](https://github.com/jeroenheijmans/sample-auth0-angular-oauth2-oidc).
139 |
140 | But nothing beats building it yourself.
141 | So here we go.
142 |
143 | ### Prerequisites
144 |
145 | To follow along, you need these things:
146 |
147 | - Node (tested with 10.8.0) and NPM (tested with 6.3.0)
148 | - Angular CLI (tested with 6.1.5)
149 | - An IDE (VS Code is nice for Angular coding)
150 | - A shell (Powershell or Bash will do)
151 |
152 | In addition you will need an [Auth0](https://auth0.com/) account.
153 | You can create one now, or when we get to that part.
154 |
155 | ### Angular setup
156 |
157 | Let's start with this:
158 |
159 | ```bash
160 | ng new DemoApp --inline-style --inline-template --skip-tests --skip-git
161 | cd DemoApp
162 | ng serve --open
163 | ```
164 |
165 | This should open a default, minimalistic Angular application.
166 | It leaves the console waiting for hot-reload requests.
167 | Next, open the `DemoApp` folder in your editor, and replace `app.component.ts` with this:
168 |
169 | ```typescript
170 | import { Component } from '@angular/core';
171 |
172 | @Component({
173 | selector: 'app-root',
174 | template: `You are logged in as {{username}}.
176 |177 | 178 | 179 | 180 |
181 |Access Token
{{token | json}}
182 | Claims
{{claims | json}}
183 | `,
184 | styles: []
185 | })
186 | export class AppComponent {
187 | username = 'TODO';
188 |
189 | get token() { return 'TODO'; }
190 | get claims() { return 'TODO'; }
191 |
192 | constructor() { }
193 |
194 | login() { }
195 | logout() { }
196 | refresh() { }
197 | }
198 | ```
199 |
200 | Your app should look somewhat like this (if you steal some of [these styles](DemoApp/src/styles.css)):
201 |
202 | 
203 |
204 | ### Adding angular-oauth2-oidc
205 |
206 | This section documents the steps to add the client side library.
207 | It's adapted from the library's readme, and the approach used in [the production-usage sample repository](https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards).
208 | Start with this on the command line:
209 |
210 | ```bash
211 | npm install --save angular-oauth2-oidc # Optionally pin to 4.0.3, which we used
212 | ```
213 |
214 | #### Changing the app.module
215 |
216 | Next, go to `app.module.ts` and make these changes:
217 |
218 | ```typescript
219 | // Existing imports...
220 | import { HttpClientModule } from '@angular/common/http'; // Added
221 | import { OAuthModule, AuthConfig, JwksValidationHandler, ValidationHandler, OAuthStorage, OAuthModuleConfig } from 'angular-oauth2-oidc'; // Added
222 | ```
223 |
224 | ```typescript
225 | // Could also go to its own file, but we just dump it next to the AppModule.
226 | const config: AuthConfig = {
227 | issuer: 'TODO',
228 | clientId: 'TODO',
229 | redirectUri: window.location.origin + '/index.html',
230 | logoutUrl: 'TODO/v2/logout?returnTo=' + encodeURIComponent(window.location.origin),
231 | silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html',
232 | scope: 'openid profile email',
233 | };
234 |
235 | config.logoutUrl = `${config.issuer}v2/logout?client_id=${config.clientId}&returnTo=${encodeURIComponent(config.redirectUri)}`;
236 | ```
237 |
238 | ```typescript
239 | // Could also go to its own file, but we just dump it next to the AppModule.
240 | const authModuleConfig: OAuthModuleConfig = {
241 | // Inject "Authorization: Bearer ..." header for these APIs:
242 | resourceServer: {
243 | allowedUrls: ['http://localhost:8080'],
244 | sendAccessToken: true,
245 | },
246 | };
247 | ```
248 |
249 | ```typescript
250 | imports: [
251 | // Existing imports...
252 | HttpClientModule, // Added
253 | OAuthModule.forRoot(authModuleConfig), // Added
254 | ]
255 | ```
256 |
257 | ```typescript
258 | providers: [
259 | { provide: OAuthModuleConfig, useValue: authModuleConfig },
260 | { provide: ValidationHandler, useClass: JwksValidationHandler },
261 | { provide: OAuthStorage, useValue: localStorage },
262 | { provide: AuthConfig, useValue: config },
263 | ]
264 | ```
265 |
266 | Some things we'll get back to once we have our Auth0 account set up.
267 |
268 | #### Changing the app.component
269 |
270 | Now let's adjust the `app.component.ts` file:
271 |
272 | ```typescript
273 | import { OAuthService, OAuthErrorEvent } from 'angular-oauth2-oidc'; // Add this import
274 | ```
275 |
276 | ```typescript
277 | // Leave @Component annotation as is...
278 | // ...just change the class:
279 | export class AppComponent {
280 | username = '';
281 |
282 | get token() { return this.oauthService.getAccessToken(); }
283 | get claims() { return this.oauthService.getIdentityClaims(); }
284 |
285 | constructor(private oauthService: OAuthService) {
286 | // For debugging:
287 | oauthService.events.subscribe(e => e instanceof OAuthErrorEvent ? console.error(e) : console.warn(e));
288 |
289 | // Load information from Auth0 (could also be configured manually)
290 | oauthService.loadDiscoveryDocument()
291 |
292 | // See if the hash fragment contains tokens (when user got redirected back)
293 | .then(() => oauthService.tryLogin())
294 |
295 | // If we're still not logged in yet, try with a silent refresh:
296 | .then(() => {
297 | if (!oauthService.hasValidAccessToken()) {
298 | return oauthService.silentRefresh();
299 | }
300 | })
301 |
302 | // Get username, if possible.
303 | .then(() => {
304 | if (oauthService.getIdentityClaims()) {
305 | this.username = oauthService.getIdentityClaims()['name'];
306 | }
307 | });
308 |
309 | oauthService.setupAutomaticSilentRefresh();
310 | }
311 |
312 | login() { this.oauthService.initImplicitFlow(); }
313 | logout() { this.oauthService.logOut(); }
314 | refresh() { this.oauthService.silentRefresh(); }
315 | }
316 | ```
317 |
318 | Let's investigate what's being changed, top to bottom.
319 | First, the `token` and `claims` properties now forward to the OAuthService.
320 | They're just for debugging purposes.
321 |
322 | Next, the `constructor` gets some good 'ole console debugging to assist while coding.
323 | It also has a "promise chain", starting with the loading of the discovery document (you could also manually enter your ID Server's config in the app, to win a bit of loading speed).
324 | This chain continues to do `tryLogin`, which inspects the browser URL to see if the hash fragment contains tokens (after a redirect back by Auth0).
325 | This will also clear the hash fragment.
326 |
327 | In typical production apps, this "chain" will be much longer.
328 | I would suggest adding an attempt to do a "Silent Refresh" to see if you can log in that way.
329 | And there are many other niceties too.
330 | Check out [this example repository](https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards) to see what such a chain would look like.
331 | For now, we'll stick with this simple setup.
332 |
333 | Finally, in the app.component, there are three methods that are just proxies for OAuthService methods.
334 |
335 | #### Silent Refreshes
336 |
337 | To get silent refreshes to work, we need to do one more thing.
338 | Add a file `silent-refresh.html` next to your `index.html` file, with these contents:
339 |
340 | ```html
341 |
342 |
343 |
344 |
345 |
346 |
347 | ```
348 |
349 | To understand why and how this works, consider how a silent refresh works for the library.
350 | In order, this will happen:
351 |
352 | 1. When a silent refresh is needed (requested, or on a timer), a hidden iframe is created.
353 | 1. That iframe is directed to Auth0, passing along (a) an instruction that no user interaction is possible, and (b) the URL of the `silent-refresh.html` file for redirecting the user back.
354 | 1. The ID Server sees the iframe, uses cookies (if any) for its domain, and checks if the user is logged in.
355 | 1. The ID Server will redirect that iframe back to the silent-refresh URL, using the hash fragment to indicate failure to log in, or otherwise passes along an access token.
356 | 1. The html above loads in the iframe, and communicates to its parent (your app!) what was passed along in the hash fragment.
357 |
358 | To get this file served as part of your Angular app, you need to add it to the `angular.json` file:
359 |
360 | ```javascript
361 | "assets": [
362 | "src/silent-refresh.html",
363 | // etc.
364 | ```
365 |
366 | **Important**: you have to restart `ng serve` for this change to be picked up!
367 |
368 | ### Setting up Auth0
369 |
370 | First things first, go [sign up for Auth0](https://auth0.com/signup).
371 |
372 | #### Creating the Client
373 |
374 | Next, go to [manage.auth0.com/#/applications](https://manage.auth0.com/#/applications) to create a Client.
375 | Hit "Create Application", name it "DemoApp", and select "Single Page Web App":
376 |
377 | 
378 |
379 | Skip the "Quick Start", and open the "Settings" tab.
380 | Take care of these base settings:
381 |
382 | - Set "Allowed Callback URLs" to `http://localhost:4200/, http://localhost:4200/index.html, http://localhost:4200/silent-refresh.html`
383 | - Set "Allowed Logout URLs" to `http://localhost:4200/, http://localhost:4200/index.html`
384 | - Note the "Client ID" (not the secret), **you will need it later**.
385 | - Note the "Domain", **you will need it later**.
386 |
387 | Then open up the "Advanced" settings at the bottom.
388 | Go to the "Grant Types" tab and note that the "Implicit" flow is checked (you could uncheck others, if you want)
389 |
390 | #### Creating the Resource (API)
391 |
392 | Go to [manage.auth0.com/#/apis](https://manage.auth0.com/#/apis) to create an API.
393 | Hit "Create API", name it "DemoApi", and give it an *Identifier*.
394 | As explained in the Auth0 UI, this should be a URI to identify your Resource API.
395 | For example, we used `https://auth0-demo-001.infi.nl`:
396 |
397 | 
398 |
399 | Note the Identifier (a.k.a. "issuer"), **you will need it later**.
400 |
401 | ### Connecting the dots
402 |
403 | Now that we have both ends set up, we need to connect them.
404 | Let's start by editing the `AuthConfig` we created earlier:
405 |
406 | - Set the `issuer` to your Auth0 domain (with `https://` added), e.g. `https://jeroenheijmans.eu.auth0.com`
407 | - Set the `clientId` to the one mentioned in the dashboard of Auth0
408 | - Set the `audience` in your `customQueryParams` to the identifier you chose earlier
409 |
410 | That's it!
411 | Reload your application (if needed), with devtools open, and you should see this:
412 |
413 | 
414 |
415 | Don't worry about all the red bits!
416 | It's supposed to be like that (for now).
417 | Try hitting the "Log in" button now, and in the subsequent Auth0 screen, choose "Sign Up":
418 |
419 | 
420 |
421 | Then authorize our Angular application:
422 |
423 | 
424 |
425 | You get redirected back to your Angular application, with tokens in the hash fragment.
426 | Before you can probably spot it, our code will log you in and clear the fragment.
427 | You should see something like this:
428 |
429 | 
430 |
431 | You're all logged in and good to go!
432 |
433 | ## Conclusions
434 |
435 | Security is hard.
436 | OAuth2 and OpenID Connect make things even more complex.
437 | But we can bring the complexity back down a bit again by using existing components, as well as a SAAS solution like Auth0.
438 |
439 | In these posts you've learned the very basics of setting up Angular and Auth0.
440 | We brought them together, ending with a simple but effective complete package.
441 |
442 | These examples were only the basics.
443 | The client side logic calls for many other more advanced features, including:
444 |
445 | - AuthGuards for protecting routes
446 | - Extended login mechanisms, trying to be as user-friendly as possible
447 | - Handling edge cases and problems
448 | - Enabling session checks (push notifications from ID Servers about Single Sign Out)
449 | - External login providers ("log in with Google", etc.)
450 | - Cross-browser-tab (or window) notifications of events like sign out
451 | - Loading user profiles with additional identity information
452 | - Preserving state (like target URL) before initializing Implicit Flow
453 |
454 | If you want to see how all these things work, you should go to [this example repository](https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards/).
455 | If you clone it and reconfigure it (change `issuer` and `clientId`, add `customQueryParams`), you should be able to "just" connect it to your own Auth0 account, and see how it works.
456 |
457 | Hopefully this post was helpful to you.
458 | It certainly was educational to *write* it.
459 |
--------------------------------------------------------------------------------