Skip to content

Commit

Permalink
Docker; Client app
Browse files Browse the repository at this point in the history
  • Loading branch information
lyphtec committed Oct 9, 2017
1 parent 5c80633 commit c5a155c
Show file tree
Hide file tree
Showing 77 changed files with 25,057 additions and 20 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,3 +1,4 @@
dist/

## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
Expand Down
53 changes: 40 additions & 13 deletions README.md
@@ -1,13 +1,11 @@
# Securing a Node API with tokens from IdentityServer4 using JWKS

This quickstart / sample shows how can you secure a Node (Express) API using [IdentityServer4](http://identityserver.io/) as your security token service.
Specifically, it uses the [JWKS](https://auth0.com/docs/jwks) endpoint and RS256 algorithm.
# Securing Node APIs with tokens from IdentityServer4 using JWKS

## Intro

The [IdentityServer4.Samples](https://github.com/IdentityServer/IdentityServer4.Samples) repo contains a sample NodeJsApi implementation. But this sample is pretty basic and uses the [express-oidc-jwks-verify](https://github.com/PDMLab/express-oidc-jwks-verify) module. I found that there are some bugs with this module, it's not very robust, and it doesn't emit meaningful error messages. It also doesn't scale very well as there is no caching and the fact that it will [write out a "tmp.crt" cert file](https://github.com/PDMLab/express-oidc-jwks-verify/blob/master/index.js#L53) to the local disk. So it's not a production quality module to use in Node apps.
This quickstart / sample shows how can you secure a Node (Express) API using [IdentityServer4](http://identityserver.io/) as the security token service.
Specifically, it uses the [JWKS](https://auth0.com/docs/jwks) endpoint and RS256 algorithm.

As such, in this sample we will use 2 higher quality modules from [Auth0](https://auth0.com/) to deal with JWTs & JWKS:
We will use 2 modules from [Auth0](https://auth0.com/) to deal with JWTs & JWKS:

- [express-jwt](https://github.com/auth0/express-jwt)
- [jwks-rsa](https://github.com/auth0/node-jwks-rsa)
Expand All @@ -21,12 +19,14 @@ IdentityServer provides a JWKS endpoint at the URI specified with the `jwks_uri`

## Repo Structure

This repo contains 3 sample projects:
This repo contains 4 sample projects:

- **idserv4** - Basic IdentityServer4 setup with Client Credentials as per the [Quickstarts #1](https://identityserver4.readthedocs.io/en/release/quickstarts/1_client_credentials.html) from the official docs. The difference being that this ASP.NET Core app is using .NET Core 2.0 and the latest version 2.0.0-rc1 of the IdentityServer4 package instead. We also setup a self-signed certificate (cert.pfx) for credential signing that will be explained below. When running, this will be accessible at http://localhost:5000
- **console** - Sample .NET Core 2.0 Console app as per the ["Creating the client"](https://identityserver4.readthedocs.io/en/release/quickstarts/1_client_credentials.html#creating-the-client) section from the docs. This app will obtain the access token from IdentityServer and will use it to call the sample Node API at http://localhost:5002/me
- **console** - Sample .NET Core 2.0 Console app as per the ["Creating the client"](https://identityserver4.readthedocs.io/en/release/quickstarts/1_client_credentials.html#creating-the-client) section from the docs. This app demonstrates using the ClientCredentials grant type to obtain the access token from IdentityServer and use it to call the sample Node API at http://localhost:5002/me
- **node-api** - Node Express based sample API which is the whole point of this repo! This is hosted at http://localhost:5002 by default. There's only 1 Javascript file (index.js) that shows the implementation, but it's documented with useful comments and should be fairly easy to follow.
- **client** - Javascript OIDC client based on the [JsOidc sample](https://github.com/IdentityServer/IdentityServer4.Samples/tree/release/Clients/src/JsOidc). This uses [webpack-dev-server](https://webpack.github.io/docs/webpack-dev-server.html) to host the client app. The "Call API" button is configured to access the Node API at http://localhost:5002/me

Two test users are configured with username/password: `bob/bob` and `alice/alice` that you can use to login.

## Setup the signing certificate

Expand All @@ -40,7 +40,9 @@ public void ConfigureServices(IServiceCollection services)
services.AddIdentityServer()
.AddSigningCredential(Config.GetSigningCertificate(_env.ContentRootPath))
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(TestUsers.Users);
}
```

Expand All @@ -64,7 +66,12 @@ public class Config
...
```

Here we are supplying a self-signed PFX (cert.pfx), generated using OpenSSL as per the [create_signing_cert.sh](https://github.com/IdentityServer/IdentityServer4.Samples/blob/release/NodeJsApi/create_signing_cert.sh) script from the NodeJsApi sample.
Included in the sample is a self-signed PFX (`cert.pfx`) generated using OpenSSL. You can generate your own self-signed certs using the following commands:

```bash
openssl req -x509 -days 365 -newkey rsa:4096 -keyout key.pem -out cert.pem
openssl pkcs12 -export -in cert.pem -inkey key.pem -out cert.pfx
```

### OpenSSL on Windows

Expand All @@ -75,7 +82,7 @@ As an aside, you can get OpenSSL for Windows [from here](http://slproweb.com/pro
Instead of using OpenSSL, you can also [use Powershell](https://www.petri.com/create-self-signed-certificate-using-powershell) to generate self-signed certs.


# Securing Node APIs
## Securing Node APIs

The main gist of how to do this is defined with the `auth` middleware in the sample Node app:

Expand Down Expand Up @@ -104,7 +111,20 @@ const auth = jwt({
```
Here we specify the IdentityServer4 instance (http://localhost:5000), and setup some options on the `jwks-rsa` module. This module supports validating the scope in the token against a specific audience (`api1` in this case).
# How to use
## How to use (with Docker)

1. Clone this repo:
```bash
git clone https://github.com/lyphtec/idserv4-node-jwk
cd idserv4-node-jwks
```
1. Run docker-compose:
```bash
docker-compose up
```
1. Access the client app at http://localhost:5005
## How to use (without Docker)

1. Clone this repo:
```bash
Expand Down Expand Up @@ -138,5 +158,12 @@ Here we specify the IdentityServer4 instance (http://localhost:5000), and setup
dotnet run
```
The decoded payload from the access token should match the claims returned from the secured Node API endpoint.
1. Run the sample client Javascript app:
```bash
cd /src/client
yarn install # (or npm install)
yarn start # (or npm start)
```
App should be up & running at http://localhost:5005
Our Node API is now secured by IdentityServer!
Our Node API is now secured by IdentityServer!
42 changes: 42 additions & 0 deletions docker-compose.yml
@@ -0,0 +1,42 @@
version: '3'

services:
idserv:
image: idserv4
container_name: idserv4
build: ./src/idserv4
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:5000/
- IS_DOCKER=true
ports:
- 5000:5000

api:
image: node-api
container_name: node-api
build: ./src/node-api
links:
- idserv
environment:
NODE_ENV: development
DEBUG: "*"
IS_DOCKER: "true"
ports:
- 5002:5002
- 9229:9229
volumes:
- ./src/node-api:/app
command: node --inspect index.js

client:
image: js-oidc-client
container_name: js-oidc-client
build: ./src/client
environment:
NODE_ENV: development
links:
- idserv
- api
ports:
- 5005:5005
9 changes: 9 additions & 0 deletions src/client/.dockerignore
@@ -0,0 +1,9 @@
node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
.vscode
dist
7 changes: 7 additions & 0 deletions src/client/Dockerfile
@@ -0,0 +1,7 @@
FROM node:8-alpine
WORKDIR /app
COPY ["package.json", "yarn.lock", "./"]
RUN yarn install
COPY . .
EXPOSE 5005
CMD ["yarn", "start"]
35 changes: 35 additions & 0 deletions src/client/package.json
@@ -0,0 +1,35 @@
{
"name": "client",
"version": "1.0.0",
"main": "webpack.config.js",
"license": "(MIT OR Apache-2.0)",
"author": "Nguyen Ly <lyphtec@gmail.com>",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/lyphtec/idsvr4-node-jwks"
},
"bugs": {
"url": "https://github.com/lyphtec/idsvr4-node-jwks/issues"
},
"scripts": {
"start": "webpack-dev-server --color --open",
"build": "webpack",
"watch": "webpack --progress --watch"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.0",
"clean-webpack-plugin": "^0.1.17",
"copy-webpack-plugin": "^4.1.1",
"html-webpack-include-assets-plugin": "^1.0.1",
"html-webpack-plugin": "^2.30.1",
"webpack": "^3.6.0",
"webpack-dev-server": "^2.9.1"
},
"dependencies": {
"bootstrap": "^3.3.7",
"oidc-client": "^1.4.1"
}
}
160 changes: 160 additions & 0 deletions src/client/src/app.js
@@ -0,0 +1,160 @@
const config = {
authority: 'http://localhost:5000/',
client_id: 'js_oidc',
redirect_uri: window.location.origin + '/callback.html',
post_logout_redirect_uri: window.location.origin + '/index.html',

popup_redirect_uri: window.location.origin + '/popup.html',
popupWindowFeatures: 'menubar=yes,location=yes,toolbar=yes,width=1200,height=800,left=100,top=100;resizable=yes',

response_type: 'id_token token',
scope: 'openid profile email api1',

// this will toggle if profile endpoint is used
loadUserInfo: true,

// silent renew will get a new access_token via an iframe
// just prior to the old access_token expiring (60 seconds prior)
silent_redirect_uri: window.location.origin + '/silent.html',
automaticSilentRenew: true,

// will revoke (reference) access tokens at logout time
revokeAccessTokenOnSignout: true,

// this will allow all the OIDC protocol claims to be visible in the window. normally a client app
// wouldn't care about them or want them taking up space
filterProtocolClaims: false
};

Oidc.Log.logger = window.console;
Oidc.Log.level = Oidc.Log.INFO;

const mgr = new Oidc.UserManager(config);

mgr.events.addUserLoaded( (user) => {
log('User loaded');
showTokens();
});
mgr.events.addUserUnloaded( () => {
log('User logged out locally');
showTokens();
});
mgr.events.addAccessTokenExpiring( () => {
log('Access token expiring...');
});
mgr.events.addSilentRenewError( (err) => {
log('Silent renew error: ' + err.message);
});
mgr.events.addUserSignedOut( () => {
log('User signed out of OP');
});

function login(scope, response_type) {
const use_popup = false;
if (!use_popup) {
mgr.signinRedirect({ scope: scope, response_type: response_type });
} else {
mgr.signinPopup({ scope: scope, response_type: response_type }).then( () => {
log('Logged in');
});
}
}

function logout() {
mgr.signoutRedirect();
}

function revoke() {
mgr.revokeAccessToken();
}

function callApi() {
mgr.getUser().then( (user) => {
const xhr = new XMLHttpRequest();
xhr.onload = (e) => {
if (xhr.status >= 400) {
display('#ajax-result', {
status: xhr.status,
statusText: xhr.statusText,
wwwAuthenticate: xhr.getResponseHeader('WWW-Authenticate')
});
} else {
display('#ajax-result', xhr.response);
}
};
xhr.open('GET', 'http://localhost:5002/me', true);
xhr.setRequestHeader('Authorization', 'Bearer ' + user.access_token);
xhr.send();
});
}

if (window.location.hash) {
handleCallback();
}

[].forEach.call(document.querySelectorAll('.request'), (button) => {
button.addEventListener('click', function() {
login(this.dataset['scope'], this.dataset['type']);
});
});

document.querySelector('.call').addEventListener('click', callApi, false);
document.querySelector('.revoke').addEventListener('click', revoke, false);
document.querySelector('.logout').addEventListener('click', logout, false);


function log(data) {
document.getElementById('response').innerText = '';

Array.prototype.forEach.call(arguments, (msg) => {
if (msg instanceof Error) {
msg = 'Error: ' + msg.message;
} else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('response').innerHTML += msg + '\r\n';
});
}

function display(selector, data) {
if (data && typeof data === 'string') {
try {
data = JSON.parse(data);
}
catch(e) {}
}
if (data && typeof data !== 'string') {
data = JSON.stringify(data, null, 2);
}
document.querySelector(selector).textContent = data;
}

function showTokens() {
mgr.getUser().then( (user) => {
if (user)
display('#id-token', user);
else
log('Not logged in');
});
}
showTokens();

function handleCallback() {
mgr.signinRedirectCallback().then( (user) => {
const hash = window.location.hash.substr(1);
const result = hash.split('&').reduce( (result, item) => {
const parts = item.split('=');
result[parts[0]] = parts[1];
return result;
}, {});

log(result);
showTokens();

window.history.replaceState({},
window.document.title,
window.location.origin + window.location.pathname);
}, (error) => {
log(error);
});
}

0 comments on commit c5a155c

Please sign in to comment.