This post demonstrates how you can protect your mobile apps and adding Open ID Connect based single sign on by integrating Red Hat Mobile with Red Hat SSO. The adapters provided by the Keycloak upstream project can be used for handing over user authentication to Red Hat SSO instead of building your own OIDC clients. Furthermore the Keycloak nodejs adapter is used to protect your Cloud App’s REST API using OIDC based access tokens. This demo project can be found here.
Prerequisites
In order for the demo to work, the following steps need to be taken:
- Install RHSSO 7.1.1 and create a realm
- Create a RHSSO client for the CloudApp
- Create a RHSSO client for the AngularJS application and create an application user
- Download and install the Nodejs and Javascript Keycloak adapters
Install Red Hat SSO 7.1.1
- Download and install RHSSO 7.1.1.
- Login as admin to RHSSO (e.g. http://localhost:8080)
- Create realm (e.g. rhmap-keycloak)
Create a Red Hat SSO client for the CloudApp
- In the rhmap-keycloak realm, create a CloudApp client (e.g. rhmap-cloudapp)
- In Settings, select Access-Type: bearer-only
- Go to Installation
- Select Keycloak OIDC JSON as Format
- Download keycloak.json and store next to application.js (your CloudApp)
Create a Red Hat SSO client for the mobile app
- In the rhmap-keycloak realm, create an AngularJS app client (e.g. rhmap-angularjs)
- In Settings, select Access-Type: public
- Go to Installation
- Select Keycloak OIDC JSON as Format
- Download keycloak.json and store in the app’s public folder
- In Settings, set up a valid redirect URI (e.g. https://localhost:8000/*)
- In Settings, set up Web Origins (e.g. https://localhost:8000). This is necessary for CORS.
- Go to the Users menu in RHSSO and add a user which will be used for logging in to the AngularJS app
Download and install Keycloak adapters
- Download and install Keycloak nodejs adapter 7.1.1 or use npm install keycloak-connect
- Download Keycloak Javascript adapter 7.1.1
- Unzip and place under public/lib in your app directory
Running the CloudApp
With the installations done and RHSSO up and running we can try out the app locally before deploying to Red Hat Mobile.
- Install grunt
npm install grunt --save-dev
- Install grunt packages:
npm install grunt-contrib-jshint --save-dev
npm install grunt-contrib-watch --save-dev
npm install grunt-env --save-dev
npm install grunt-nodemon --save-dev
npm install grunt-ng-constant --save-dev
- Run the CloudApp with
grunt
orgrunt serve:local
This will start the CloudApp on localhost:8000. You can verify that it works by opening a browser and try to access http://localhost:8000/api/ping. You should see “pong” as a response.
The CloudApp also contains a resource that is protected by RHSSO:
app.get('/api/protected', keycloak.protect(), function (req, res) {...})
A simple test that the Keycloak Nodejs adapter is properly installed is to open a browser and try to access http://localhost:8000/api/protected. You should see an Access Denied message.
Calling this method from the AngularJS app will require that an OIDC access token is passed in a header, which will be described further below.
Securing a mobile app with Red Hat SSO
To add user authentication to the mobile app, and have Red Hat SSO provide the login page, a Keycloak adapter is used. Since the demo is based on the AngularJS framework, we use the Javascript Keycloak adapter.
Note that the app needs to be bootstrapped manually in order to prevent
constant reloading due to the redirect sent back from RHSSO after authentication:
angular.element(document).ready(() => { window._keycloak = Keycloak(); window._keycloak .init({ onLoad: 'login-required' }) .success(() => { angular.bootstrap(document, ['rhmap-keycloak']); // manually bootstrap Angular app }); });
The _keycloak
variable can be accessed by other modules by injecting a service:
angular .module('auth', ['ui.router']) .service('authService', ['$window', function($window) { return $window._keycloak; }]);
Once the user is authenticated with RHSSAO the mobile app makes an attempt to make a REST call to the Cloud App.
When calling a protected resource in the Cloud App, the Authorization header must be added:
When the CloudApp runs locally:
$http.get('/api/protected', {headers:{'Authorization': 'bearer ' + authService.token}});
When the CloudApp runs on Red Hat Mobile, the $fh.cloud API is used instead of $http. Note also the use of defer in order to return a promise to the controller in the same way as $http does.
$fh.cloud({ "path": "/api/protected", "method": "GET", "contentType": "application/json", "headers": {"Authorization": 'bearer ' + authService.token}, "timeout": 25000 }, function(res) { deferred.resolve(res); }, function(msg,err) { deferred.reject({status: err.status, statusText: msg}); });
Running the demo
Start the CloudApp locally with grunt serve:local or just grunt.
Open a browser at the location of your CloudApp, e.g. http://localhost:8000
If you haven’t authenticated to RHSSO a login prompt served from RHSSO will be shown:
After logging in with the user you created in RHSSO you will be able to access the AngularJS app
The app will immediately try to access the protected CloudApp resource /api/protected
using the access token provided by the Javascript Keycloak adapter. If everything works you should see an alert box containing “Response from protected resource, data:”OK”, status: 200 and the access token.
Deploying to Red Hat Mobile
When deploying the demo on RHMAP it’s necessary to also redeploy RHSSO on a routable server in order for the Keycloak nodejs adapter used to protect the REST API of the CloudApp to be able to validate the access token.
Deploying a routable Red Hat SSO instance
In order for the CloudApp running on Red Hat Mobile to be able to access RHSSO it needs to be deployed on a server with a routable IP address. Also since RHSSO is intended to be accessible on the 127.0.0.1 address only, it is recommended to install a reverse proxy on the same host to provide an external URI to RHSSO. In this demo I have used HAProxy which is installed on RHEL with yum install haproxy
.
HAProxy is then configured to forward requests to RHSSO as follows:
/etc/haproxy/haproxy.cfg: frontend http_web *:80 mode http default_backend rgw backend rgw balance roundrobin mode http stats enable stats hide-version stats uri /stats stats realm Haproxy\ Statistics stats auth haproxy:redhat server keycloak 127.0.0.1:8080 check
The global and default sections can be left with default settings.
For a full description on how to configure HAProxy refer to the HAProxy docs.
You can access HAProxy statistics on the server (e.g. http://rhsso-host/stats) using haproxy/redhat as username/password. You should now be able to access RHSSO on http://rhsso-host:80 and log in to the Admin app.
To support running behind a reverse proxy, RHSSO must be configured to read the client’s IP address from the X-Forwarded-For header. This is done by adding the attribute proxy-address-forwarding="true"
to the http-listener element in the undertow subsystem in the standalone.xml configuration file.
[code language=”xml”]
<subsystem xmlns="urn:jboss:domain:undertow:3.1">
<buffer-cache name="default"/>
<server name="default-server">
<http-listener name="default" socket-binding="http" redirect-socket="https" proxy-address-forwarding="true"/>
[/code]
Also haproxy.cfg must contain option forwardfor
in order to properly set the client’s IP in the X-Forwarded-For header.
defaults option forwardfor except 127.0.0.0/8
Finally, the rhmap-angularjs client in RHSSO needs to be configured with valid redirect URI set to the host name of the CloudApp which is created when deployed to RHMAP. The Web Origins for the rhmap-angularjs client needs to be set to the public IP from which the AngularJS app is running, or to “*” to support a mobile device with changing IP addresses.
When RHSSO and HAProxy is up and running, the next step is to deploy the CloudApp on Red Hat Mobile.
Deploying the Cloud App
To deploy the CloudApp on RHMAP do the following steps:
- In App Studio go to Projects then choose Import.
- Create New Project
- Select Empty Project
- Select App Type = Cloud App
- Import from public git repo and use the link to this repo: https://github.com/torbjorndahlen/rhmap-keycloak.git
- Go to the CloudApp’s Editor menu and open the file
rhmap-keycloak/public/js/util/config.js
- Change the line
.constant('ENV', {name:'local',apiEndpoint:'http://localhost:8000'})
to.constant('ENV', {name:'remote',apiEndpoint:''})
and save the file - Create a connection tag which requires a Client App. Simply go to Projects and create a Hello World App in the project.
- Go to Connections and create a new Connection
- Select Configure and copy everything in the JSON object into the file
fhconfig
which is located underrhmap-keycloak/public
. - The
auth-server-url
in keycloak.json for both the CloudApp and the AngularJS app needs to be
changed to the server and port where HAProxy is running. - Deploy the Cloud App
- Use the link to the Cloud App to access the mobile app
Troubleshooting
Chrome adding Authorization Basic header
Note that trying to login to RHSSO from Chrome (v.59.0.3071.115) causes an infinite loop when RHSSO is deployed on a routable server behind HAProxy. This is since Chrome adds an Authorization Basic
header to the request.
Request received by HAProxy when using Chrome:
localhost haproxy[14515]: 83.233.154.15:49283 [19/Jul/2017:00:48:16.362] http_web rgw/keycloak 1/0/0/272/+273 200 +333 - - ---- 4/4/1/0/0 0/0 {Basic aGFwcm94eTpyZWRoYXQ=} "GET /auth/ HTTP/1.1"
This can be solved by using for example Safari.
Request received by HAProxy when using Safari:
localhost haproxy[14515]: 83.233.154.15:49658 [19/Jul/2017:00:49:53.359] http_web rgw/keycloak 943/0/0/7/+950 200 +215 - - ---- 1/1/1/0/0 0/0 {} "GET / HTTP/1.1"
No “Access-Control-Allow-Origin”
If you’re seeing No “Access-Control-Allow-Origin” in the browsers web console when the AngularJS app
tries to authenticate to RHSSO (look for requests to http://localhost:8080/auth/realms/rhmap-keycloak),
you should ensure you have added the AngularJS app as Web Origin when creating a client for AngularJS in RHSSO.
Test for access tokens
Using curl to obtain an access token:
$ RESULT=`curl --data "grant_type=password&client_id=rhmap-keycloak&username=user&password=password" http://localhost:8080/auth/realms/rhmap-keycloak/protocol/openid-connect/token` $ echo $RESULT $ TOKEN=`echo $RESULT | sed 's/.*access_token":"//g' | sed 's/".*//g'` $ curl http://localhost:8000/api/protected -H "Authorization: bearer $TOKEN"
First we authenticate with our realm in RHSSO. Note that Direct-Access-Grants must be enabled in the Realm Settings in order to authenticate with username and password parameters.
Next we parse out the access token and stores it in the TOKEN variable.
Finally we call the Cloud App’s protected resource with the access token.