Codementor Events

Passport-JWT Authentication for Hyperledger Composer Rest Server

Published May 28, 2018Last updated Nov 23, 2018
Passport-JWT Authentication for Hyperledger Composer Rest Server

Introduction

Hyperledger Composer is a framework for developing Blockchain Applications on the Hyperledger Fabric Blockchain Platform.

We recently covered Getting Started with Hyperledger Composer, and I would recommend you go through that article if you are not familiar with the technology.

In this article, we will do the following:-

  • Create a sample business network definition
  • Generate a Composer Rest Server with Docker and PassportJWT authentication
  • Consume the Authenticated API using a custom JWT token.

To understand why this is not as straightforward, you can look at this GitHub issue , which I've been following closely.

If you are familiar with Hyperledger Composer and you have a sample network, you can jump directly to the Hyperledger Composer Rest Server Section.

Prerequisites

To follow along, you should have the following installed.

1. Hyperledger Composer Prerequisites

This includes NodeJS LTS version and Docker. The instructions for your specific platform can be found in this documentation.

Once both are installed, you can confirm they are working properly with:-

docker -v
Docker version 18.03.1-ce, build 9ee9f40

node -v
v8.11.2

2. Hyperledger Composer Development Environment

This includes the Composer CLI client:- composer-cli, Yeoman Generator: generator-hyperledger-composer and yo and the Hyperledger Fabric Docker images. We'll look at the Hyperledger Fabric Docker Images in the Hyperledger Fabric Instance Section.

npm install -g composer-cli yo generator-hyperledger-composer

If you are using Visual Studio Code for your development work, you may want to install the Hyperledger Composer plugin.

You can confirm that composer-cli is installed successfully with typing in composer command on your terminal:

composer -v
v0.19.5

Hyperledger Fabric Instance

Before we begin, we need to be running an instance of Hyperledger Fabric. The Hyperledger Composer Development Environment Guide has the instructions on how to start a network, but we'll cover it briefly.

Create a directory called fabric-dev-server, and inside it while on the terminal, pull the sample fabric-dev-server provided by Hyperledger Composer.

cd fabric-dev-server

curl -O https://raw.githubusercontent.com/hyperledger/composer-tools/master/packages/fabric-dev-servers/fabric-dev-servers.tar.gz

tar -xvf fabric-dev-servers.tar.gz

You should have the following files downloaded, that will help you quickly spin up a Hyperledger Fabric instance.

ls
DevServer_connection.json createComposerProfile.sh  downloadFabric.sh         fabric-scripts            startFabric.sh            teardownAllDocker.sh
_loader.sh                createPeerAdminCard.sh    fabric-dev-servers.tar.gz package.json              stopFabric.sh             teardownFabric.sh

To start an instance, first download the Hyperledger Fabric images.

./downloadFabric.sh

You will notice the following new Docker images in your system.

docker images
REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
hyperledger/fabric-ca        x86_64-1.1.0        72617b4fa9b4        2 months ago        299MB
hyperledger/fabric-orderer   x86_64-1.1.0        ce0c810df36a        2 months ago        180MB
hyperledger/fabric-peer      x86_64-1.1.0        b023f9be0771        2 months ago        187MB
hyperledger/fabric-ccenv     x86_64-1.1.0        c8b4909d8d46        2 months ago        1.39GB
hyperledger/fabric-couchdb   x86_64-0.4.6        7e73c828fc5b        3 months ago        1.56GB

Next, start a Hyperledger Fabric instance.

./startFabric.sh

These scripts should take a couple of minutes to complete. It creates a composer_default Docker network, and runs the containers within that network so that they can communicate via their custom hostnames. Once done, you can check the status of the running Hyperledger Fabric by checking the running Docker containers.

docker ps
CONTAINER ID        IMAGE                                     COMMAND                  CREATED             STATUS              PORTS                                            NAMES
4d59ab99ac8c        hyperledger/fabric-peer:x86_64-1.1.0      "peer node start"        12 seconds ago      Up 19 seconds       0.0.0.0:7051->7051/tcp, 0.0.0.0:7053->7053/tcp   peer0.org1.example.com
5985d3ceeb34        hyperledger/fabric-ca:x86_64-1.1.0        "sh -c 'fabric-ca-se…"   14 seconds ago      Up 20 seconds       0.0.0.0:7054->7054/tcp                           ca.org1.example.com
1163c18322f3        hyperledger/fabric-couchdb:x86_64-0.4.6   "tini -- /docker-ent…"   14 seconds ago      Up 19 seconds       4369/tcp, 9100/tcp, 0.0.0.0:5984->5984/tcp       couchdb
0d4c964f8dc5        hyperledger/fabric-orderer:x86_64-1.1.0   "orderer"                14 seconds ago      Up 20 seconds       0.0.0.0:7050->7050/tcp                           orderer.example.com

The final step when setting up a Hyperledger Fabric instance for composer is to generate credentials. Composer allows you to do this by saving the relevant information in a .card file. This file is usually generated based on a connection-profile, which is defined in a JSON file. The fabric-dev-server we downloaded has such as file in the file fabric-dev-server/fabric-scripts/hlfv11/createPeerAdminCard.sh.

Edit the section that creates the connection-profile (Line 78 at the time of writing). It starts with

cat << EOF > DevServer_connection.json

Edit the content to use host names, instead of the default ${HOST}. The properties we are editing are the orderers, peers and the certificationAuthorities.

createPeerAdminCard.sh

...
cat << EOF > DevServer_connection.json
{
    "name": "hlfv1",
    "x-type": "hlfv1",
    "x-commitTimeout": 300,
    "version": "1.0.0",
    "client": {
        "organization": "Org1",
        "connection": {
            "timeout": {
                "peer": {
                    "endorser": "300",
                    "eventHub": "300",
                    "eventReg": "300"
                },
                "orderer": "300"
            }
        }
    },
    "channels": {
        "composerchannel": {
            "orderers": [
                "orderer.example.com"
            ],
            "peers": {
                "peer0.org1.example.com": {}
            }
        }
    },
    "organizations": {
        "Org1": {
            "mspid": "Org1MSP",
            "peers": [
                "peer0.org1.example.com"
            ],
            "certificateAuthorities": [
                "ca.org1.example.com"
            ]
        }
    },
    "orderers": {
        "orderer.example.com": {
            "url": "grpc://orderer.example.com:7050"
        }
    },
    "peers": {
        "peer0.org1.example.com": {
            "url": "grpc://peer0.org1.example.com:7051",
            "eventUrl": "grpc://peer0.org1.example.com:7053"
        }
    },
    "certificateAuthorities": {
        "ca.org1.example.com": {
            "url": "http://ca.org1.example.com:7054",
            "caName": "ca.org1.example.com"
        }
    }
}
EOF
...

The difference here with the original files is that we've replaced the ${HOST} with custom hostnames. We are doing his since Composer Rest Server will be running as a Docker container, and it will use this hostnames to access the relevant containers within the network.

Note, however, that we still need to access the peers, certification authorities and orderers from the terminal. They have been exposed on our host by Docker, and are being listenend to on specific ports on localhost. We therefore need to add records of these hostnames to our /etc/hosts file, so that they can resolve to localhost when called from within our host. You might need to use sudo.

/etc/hosts

##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1	localhost
255.255.255.255	broadcasthost
::1             localhost
127.0.0.1 orderer.example.com peer0.org1.example.com ca.org1.example.com

Now, we can proceed to create the peer admin card by running this in the fabric-dev-server directory.

./createPeerAdminCard.sh

To check existing cards, run

composer card list
The following Business Network Cards are available:

Connection Profile: hlfv1
┌─────────────────┬───────────┬──────────────────┐
│ Card Name       │ UserId    │ Business Network │
├─────────────────┼───────────┼──────────────────┤
│ PeerAdmin@hlfv1 │ PeerAdmin │                  │
└─────────────────┴───────────┴──────────────────┘


Issue composer card list --card <Card Name> to get details a specific card

Command succeeded

Business Network

Next, we are going to create a sample business network. We'll call it 'tutorial-network', the same name given in the Hyperledger documentation.

Our business network will be the default one generated by Hyperledger Yeoman generator. We are not changing it since the main focus here is to authenticate the Composer Rest Server.

We'll use the generator-hyperledger-composer we installed when we started. Use the following, and replace whenever relevant.

yo hyperledger-composer:businessnetwork
Welcome to the business network generator
? Business network name: tutorial-network
? Description: tutorial network
? Author name:  Christopher Ganga
? Author email: ganga.chris@gmail.com
? License: Apache-2.0
? Namespace: org.example.biznet
? Do you want to generate an empty template network? No: generate a populated sample network
   create package.json
   create README.md
   create models/org.example.biznet.cto
   create permissions.acl
   create .eslintrc.yml
   create features/sample.feature
   create features/support/index.js
   create test/logic.js
   create lib/logic.js

This generates a directory called tutorial-network, with the following content.

.
├── README.md
├── features
│   ├── sample.feature
│   └── support
│       └── index.js
├── lib
│   └── logic.js
├── models
│   └── org.example.biznet.cto
├── package.json
├── permissions.acl
└── test
   └── logic.js

5 directories, 8 files

Deploy Business Network

Once the business network is created, we continue to build the archive file, and deploy it to the existing running instance of Hyperledger Fabric.

In the root of the tutorial-network directory, create the business network archive.

composer archive create -t dir -n .

This creates the archive file tutorial-network@0.0.1.bna.

Now that we have the archive file, we can proceed to install the network into the peers. Run this command to install the network:

composer network install --card PeerAdmin@hlfv1 --archiveFile tutorial-network@0.0.1.bna

Next, we start the network by running:

composer network start --networkName tutorial-network --networkVersion 0.0.1 --networkAdmin admin --networkAdminEnrollSecret adminpw --card PeerAdmin@hlfv1 --file networkadmin.card

This command might take a while to run, but when it completes, you will notice a new Docker container running with the name starting with dev-peer0.org1.example.com-tutorial-network-0.0.1... which will be running our business network. You can check this by running docker ps.

We then need to import the new networkadmin card, to make it usable in the network.

composer card import --file networkadmin.card

You get back a networkadmin card name admin@tutorial-network, which we can use moving forward. You can also check this by running composer card list

Finally, to ensure the business network is deployed successfully, we can ping it with

composer network ping --card admin@tutorial-network

Business Network Default Participant.

Since the goal of this article is to create an authenticated version of the Composer Rest Server, we are going to create a default participant, who will be able to create other particiapnts and identities.

The structure of our participant in the /fabric-dev-servers/tutorial-network/models/org.example.biznet.cto file is:

participant SampleParticipant identified by participantId {
  o String participantId
  o String firstName
  o String lastName
}

We'll create a participant and bind it to an identity so that we get an identity card for this participant. This is the participant we'll add.

{ 
  "$class": "org.example.biznet.SampleParticipant", 
    "participantId": "gangachris", 
    "firstName": "chris", 
    "lastName": "ganga"
}

Define the participant data in your terminal.

participantId=gangachris
firstName=chris
lastName=ganga
PARTICIPANT_CLASS=org.example.biznet.SampleParticipant
participantData="{\"\$class\": \"${PARTICIPANT_CLASS}\", \"participantId\": \"${participantId}\", \"firstName\": \"${firstName}\", \"lastName\": \"${lastName}\"}"

Then add the participant to the network.

composer participant add -c admin@tutorial-network  -d "$participantData"

Then we need to issue an identity to this participant, so that they have an identity card.

composer identity issue -c admin@tutorial-network -f gangachris.card -u $participantId -a "resource:${PARTICIPANT_CLASS}#${participantId
}" -x true

The -x flag allows the participant to be able to issue identities.

Hyperledger Composer Rest Server (Docker)

The most common production way to deploy Composer Rest Server is through Docker. This is mainly because a Hyperldger Fabric instances deployments are mostly run in container networked ecosystem either with Kubernetes, Docker. Swarm or normal docker-compose styles like we are doing now.

Since we are using passport-jwt, we'll need to create a custom JWT file that we will add inside the Docker container.

custom-jwt.js

// based on:
// https://github.com/hyperledger/composer/issues/2038

const passportJwt = require('passport-jwt');
const util = require('util');

function CustomJwtStrategy(options, verify) {
  options.jwtFromRequest = passportJwt.ExtractJwt.fromAuthHeaderAsBearerToken();
  passportJwt.Strategy.call(this, options, verify);
}

util.inherits(CustomJwtStrategy, passportJwt.Strategy);

module.exports = {
  Strategy: CustomJwtStrategy
};

We are not adding any JWT claims here, as we are trying to make it as simple as possible. We retrieve the JWT as bearer token from the request authorization header (passportJwt.ExtractJwt.fromAuthHeaderAsBearerToken()), and the library will handle the rest of the authentication.

Note that we are going to generate a JWT token from somewhere else, probably a client that will be consuming the Composer Rest Server. For Composer Rest Server to decode the token and give us its own access token, the JWT secret used to generate the original JWT token must be used as the secret variable for the passport-jwt configuration, which we will see shortly.

Composer Rest Server also requires that the JWT token have a claim of either ID or username. (Not sure why this is the case).

We'll then build a Docker image for the composer rest server. Create a Dockerfile.

Dockerfile

FROM hyperledger/composer-rest-server:0.19.5

RUN npm install --production loopback-connector-mongodb passport-jwt && \
    npm cache clean --force && \
    ln -s node_modules .node_modules

COPY custom-jwt.js node_modules/custom-jwt.js

COPY .composer /home/composer/.composer

We will install passport-jwt and loopback-connector-mongodb as part of the image build. We then add the custom-jwt.js file we created earlier.

Composer default directory for identity cards is /home/composer/.composer. Before we build the image, we can copy the existing identity cards from our home/composer/.composer directory. We also need to change the file permissions so that docker can handle them without EACCESS errors.

cp -rp ~/.composer .

chmod -R a+rw .composer
find .composer -type d -exec chmod a+x {} +

We can now build the Docker image.

docker build -t localhost/composer-rest-server .

Notice we installed loopback-connector-mongodb as part of the image build. This means we are going to use MongoDB as the database of choice to store our authenticated users and credentials. We therefore need a running instance of MongoDB.

This instance, however, needs to be in the same Docker network as the current running Hyperledger Fabric network. When we started the Hyperledger Fabric instance, a network called composer_default was created. To confirm this, we can run docker network ls.

docker network ls
NETWORK ID          NAME                     DRIVER              SCOPE
e7da7b74070e        bridge                   bridge              local
bdae606dc573        composer_default         bridge              local
f0f15a5d479b        host                     host                local
e9b986a1141d        none                     null                local

You can see we have a composer_default network. To inspect the containers that are running withing this network we can run, and check the Containers property.

docker network inspect composer_default

You'll notice the containers are the ones that are currently running,

"Containers": {
            "0d4c964f8dc5f7cc8acb48a48d5714d6d3a9349b02992c1d60cc87f0d2d0dd2b": {
                "Name": "orderer.example.com",
                "EndpointID": "3a20963d2a31802e9769448f3a880d9b54369924eac27943e604e191f68c5a8b",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            },
            "1163c18322f3194a0df08d40f11cc134ac006a06221091504d7bfee7cc73c13d": {
                "Name": "couchdb",
                "EndpointID": "1a9f4de2748134bcf1a9c0cb4bbaf06d9e06391eee2b741db163f4165553dfa4",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
               },
            ....
}

Run the mongodb container in the composer_default network.

docker run -d --name mongo --network composer_default -p 27017:27017 mongo

Finally, to run the composer-rest-server container, we need to pass in some environment variables so that the Composer Rest Server runs in a multi-user mode, and allows authentication based on passport-jwt.

Create a file envvars.txt.

COMPOSER_CARD=admin@tutorial-network
COMPOSER_NAMESPACES=never
COMPOSER_AUTHENTICATION=true
COMPOSER_MULTIUSER=true
COMPOSER_PROVIDERS='{
  "jwt": {
    "provider": "jwt",
    "module": "/home/composer/node_modules/custom-jwt.js",
    "secretOrKey": "gSi4WmttWuvy2ewoTGooigPwSDoxwZOy",
    "authScheme": "saml",
    "successRedirect": "/",
    "failureRedirect":"/"
    }
}'
COMPOSER_DATASOURCES='{
  "db": {
    "name": "db",
    "connector": "mongodb",
    "host": "mongo"
  }
}'

The CUSTOM_PROVIDERS environment variable specifies that we are using jwt, and a custom module.

Since we are using MongoDB, our CUSTOM_DATASOURCES environment variable points to this too.

To make these variables become actual environment variables, we run

source envvars.txt

Then we can run the Docker container in the composer_default network.

docker run \
    -d \
    -e COMPOSER_CARD=${COMPOSER_CARD} \
    -e COMPOSER_NAMESPACES=${COMPOSER_NAMESPACES} \
    -e COMPOSER_AUTHENTICATION=${COMPOSER_AUTHENTICATION} \
    -e COMPOSER_MULTIUSER=${COMPOSER_MULTIUSER} \
    -e COMPOSER_PROVIDERS="${COMPOSER_PROVIDERS}" \
    -e COMPOSER_DATASOURCES="${COMPOSER_DATASOURCES}" \
    --name rest \
    --network composer_default \
    -p 3000:3000 \
    localhost/composer-rest-server

To make sure the composer rest server is running successfully, we check its logs.

docker logs rest
[2018-05-22 22:24:44] PM2 log: Launching in no daemon mode
[2018-05-22 22:24:44] PM2 log: Starting execution sequence in -fork mode- for app name:composer-rest-server id:0
[2018-05-22 22:24:44] PM2 log: App name:composer-rest-server id:0 online
WARNING: NODE_APP_INSTANCE value of '0' did not match any instance config file names.
WARNING: See https://github.com/lorenwest/node-config/wiki/Strict-Mode
Discovering types from business network definition ...
Discovered types from business network definition
Generating schemas for all types in business network definition ...
Generated schemas for all types in business network definition
Adding schemas for all types to Loopback ...
Added schemas for all types to Loopback
Web server listening at: http://localhost:3000
Browse your REST API at http://localhost:3000/explorer

If you see the last line Browse your REST API at http://localhost:3000/explorer, we are good.

Visiting http://localhost:3000/explorer and trying to do a GET on any of the endpoints returns a 401 Unauthorized
Screen Shot 2018-05-23 at 01.26.12.png.

Authenticating Composer Rest Server

To authenticate the composer rest server, we need to do the following:-

  1. Call the route http://localhost:3000/auth/jwt/callback with a valid jwt token passed as a Bearer token.
  2. Retrieve the access token from the cookies, since this is how the response comes back.
  3. Use the retrieved token to import an identity card through the route http://localhost:3000/api/wallet/import
  4. Call other endpoints.

We'll be using Postman for this exercise, since it's one of the intuitive ways to pass in a header to a request, so we will not really do number two, since the cookies will be passed in subsequent requests.

But if you have a client consuming the composer-rest server, you need to retrieve the token from the access_token cookie that comes back. Here's some sample code to retreive the token, this is from Hyperledger fabric. We may look at building such clients in another article.

I generated a JWT token based on the above secret gSi4WmttWuvy2ewoTGooigPwSDoxwZOy, with the following claims: "timestamp":time.Now().UnixNano(),"username": username. The syntax is for Golang, but the claims are the current timestamp, and the username is gangachris. The resulting token is this.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE1MjcxNjI1MDQ3NDI1NjUwODcsInVzZXJuYW1lIjoiZ2FuZ2FjaHJpcyJ9.WyARsOhMSDVRjUpd-rPBI1A8-Vpz7pDS6rICXKN8W3U

Note that this token is however not secure, as it doesn't have enough claims, and an expiry. This is strictly for demo purposes.

Next, we make a GET request http://localhost:3000/auth/jwt/callback with the Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE1MjcxNjI1MDQ3NDI1NjUwODcsInVzZXJuYW1lIjoiZ2FuZ2FjaHJpcyJ9.WyARsOhMSDVRjUpd-rPBI1A8-Vpz7pDS6rICXKN8W3U as the header. This header can be easily added in the Authorization tab for Postman.
Screen Shot 2018-05-24 at 14.55.41.png

Notice we get back a response with three cookies. connect.sid, access_token and userId. If you are familiar with JavaScript, you can now see how the code I shared earlier can be used to extract the access_token for subsequent requests.

If you now remove the authorization header and call http://localhost:3000/api/SampleAsset, you don't get a 401 Unauthorized error, but 500 A business network card has not been specified.
Screen Shot 2018-05-24 at 15.01.16.png.

To import a card, we make a POST request to http://localhost:3000/api/Wallet/import, and attach the identity card we generated when we created a participant, and a name, in this case gangachris.
Screen Shot 2018-05-24 at 15.04.36.png

Notice we get back a 204 No Content response.

To confirm that the card has been imported successfully, we call http://localhost:3000/api/Wallet.
Screen Shot 2018-05-24 at 15.05.35.png

We get back a response with a list of wallets, and the default one being used currently.

At this this stage, we are successfully authenticated, and call the http://localhost:3000/api/SampleAsset route without any errors.
Screen Shot 2018-05-24 at 15.05.56.png.

Things to Note

  1. The only reason we are able to call these requests after calling http://localhost:3000/auth/jwt/callback is because Postman is using the cookies it got on this request for subsequent requests.

  2. This will not work when you have a custom client. In this case, you will have to retrieve the access token via code like this one, and then make subsequent calls with either a header or a query parameter like below.

curl -v http://localhost:3000/api/system/ping?access_token=xxxxx

or

curl -v -H 'X-Access-Token: xxxxx' http://localhost:3000/api/system/ping

The first request after authentication must be a POST to http://localhost:3000/api/Wallet/import, passing along an authentication card and a name. It makes sense to call http://localhost:3000/api/Wallet to check if the default wallet and ensure it's the right one.

  1. Once authenticated, you can create other Participants by posting to the participant route. In this case its POST http://localhost:3000/api/SampleAprticipant. Then to issue them an identity, you have to make another post request to http://localhost:3000/api//system/identities/issue, with the relevant parameters, which will give you back a binary response, and you have to save that to a .card file.

  2. To avoid using Postman, you may change the way the jwt token is retrieved by the passport-jwt in the custom-jwt.js file we created using passportJwt.ExtractJwt.fromUrlQueryParameter("token");

function CustomJwtStrategy(options, verify) {
  options.jwtFromRequest = passportJwt.ExtractJwt.fromUrlQueryParameter("token");
  passportJwt.Strategy.call(this, options, verify);
}

Once this is changed, you can visit http://localhost:3000/auth/jwt/callback?query=<token>, and you'll get back the swagger page.

Conclusion

Hyperledger Composer is a good resource to get you up and running with the Hyperledger Fabric blockchain.

You may have an existing system and you'd like to integrate Hyperledger Fabric, and this article has briefly covered one of the ways to achieve this.

Happy Coding.

Discover and read more posts from Christopher Ganga
get started
post commentsBe the first to share your opinion
Michael Clayton
5 years ago

This article shows you how to configure a REST server for multi-user mode and how you can call it from a client application to add members and identities, as well as send transactions signed with different identities. The client application is written using Angular and will use GitHub authentication to authenticate with the REST server. This is just one example of the type of authentication you can use. The REST server uses an open source library called Passport and more than 300 plug-ins are available, for example. LDAP, Facebook, SAML and Google. https://coincase.ru/blog/47419/

Rafael Sanchez G
5 years ago

Hi Christopher,

Thanks for the article,

I successfully consume the Authenticated API using the custom JWT token in Postman.

But when I tried to consume from angular httpClient, I can’t save the cookies (access token), because 302 redirection to http://localhost:3000/explorer/.

Any suggestion?

Ilham Qasse
4 years ago

Did you successfully get the cookies from the angular?
Im facing the same issue.

Chandrakanth Bairy
5 years ago

We authenticate one user using a secret and generating bearer JWT token and importing the card using JWT as a cookie.
Then we start a composer rest server after setting environment variables for the jwt authentication.
Then we proceed further using the cookies generated for the other calls.
So say that another user wants to authenticate himself in the network using a different secret key rather than authenticating himself using the same bearer jwt token produced from above case. In this case, I can’t figure out how to proceed further.
After the generation of jwt token for each user based on his own secret key, how do I validate the token in already running composer rest server?

Show more replies