Secured nodejs backend API's using keycloak - node.js

I'm implementing a Nodejs backend API's. Some of them are need to authenticate before access. for that I choose keycloak server as identity server. I used npm keycloak-connect library to integrate node server and the keycloak server.
Now the authentication are woking fine.
problem is when I logout from keycloak server by using 'http://localhost:8080/auth/realms/test-realm/protocol/openid-connect/logout' this API. keycloak server says token is not valid anymore. But when I used the same taken to access Node server it takes that token as valid token.
'use strict';
const Keycloak = require('keycloak-connect');
const express = require('express');
var cors = require('cors')
const app = express();
app.use(cors())
var keycloakConfig ={
"realm": "test-realm",
"auth-server-url": "http://localhost:8080/auth",
"ssl-required": "external",
"resource": "test-dev-api",
"public-client": true,
"confidential-port": 0
}
var keycloak = new Keycloak({},keycloakConfig);
app.use( keycloak.middleware( { logout: '/logout'} ));
app.get('/secured-echo', keycloak.protect(), function(req,resp) {
resp.send("Secured Hello");
});
//unprotected route
app.get('/echo', function(req,resp) {
console.log(keycloakConfig)
console.log(keycloak)
resp.json({"say": "hello"});
});
app.listen(4000, function () {
console.log('Listening at port:4000');
});

Your application uses Express, which maintains its own sessions and synchronizes those with Keycloak tokens. So logging out on Keycloak does not tell Express that you have logged out. Subsequently you can still log in to your application.
In your code you have specified this:
app.use( keycloak.middleware( { logout: '/logout'} ));
Which is the URL on your application to logout in your Express application. Use that instead of directly logging out of Keycloak. The keycloak middleware will then log out in Keycloak. It will be something like
http://localhost:4000/logout

I think you should use app.use( keycloak.middleware( { logout: '/logout'} )); this one before
app.listen(4000, function () {
console.log('Listening at port:4000');
});

Related

Keycloak / Nodejs integration raise 400 Bad Request

I'm evaluating Keycloak integration to secure the access of a Nodejs API. I followed different tutorials such as:
Keycloak documentation
Keycloak quick start
But I'm facing an issue while restricting access to a route with a specific role.
I created a realm named 'tst', a client named 'tst-api' with the following URLs:
redirect URL: http://localhost:3000/*
web origins URL: http://localhost:3000
client is configured with client authentication, standard flow and direct access grant capabilities.
I created two realm Roles: realm-tst-user and realm-tst-admin and also two client roles: client-tst-user and client-tst-admin
I associated client roles to realm roles:
client-tst-user -> realm-tst-user
client-tst-admin -> realm-tst-admin
and I create a user with a role mapping set to 'realm-tst-user'
I'm able to generate a token with CURL command:
curl -X POST 'http://localhost:8080/realms/tst/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=tst-api' \
--data-urlencode 'client_secret=<SECRET_FROM_CLIENT_CREDENTIALS>' \
--data-urlencode 'username=user' \
--data-urlencode 'password=1234'
So I assume everything is ok for this. Then, I implemented a very basic node API based on the tutorial mentioned just above:
My index.js is the following:
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const Keycloak = require('keycloak-connect');
const cors = require('cors');
const app = express();
app.use(bodyParser.json());
// Enable CORS support
app.use(cors());
const memoryStore = new session.MemoryStore();
app.use(session({
secret: 'session',
resave: false,
saveUninitialized: true,
store: memoryStore
}));
const keycloak = new Keycloak({
store: memoryStore
});
app.use(keycloak.middleware({
logout: '/logout',
admin: '/'
}));
app.get('/service/public', function (req, res) {
res.json({message: 'public'});
});
app.get('/service/secured', keycloak.protect('client-wib-user'), function (req, res) {
res.json({message: 'secured'});
});
app.get('/service/admin', keycloak.protect('client-wib-admin'), function (req, res) {
res.json({message: 'admin'});
});
app.listen(3000, function () {
console.log('Started at port 3000');
});
And keycloak.json is the following:
{
"realm": "tst",
"auth-server-url": "http://localhost:8080/",
"ssl-required": "external",
"resource": "tst-api",
"verify-token-audience": true,
"credentials": {
"secret": "<SECRET_FROM_CLIENT_CREDENTIALS>"
},
"use-resource-role-mappings": true,
"confidential-port": 0,
"policy-enforcer": {}
}
When I try to access a secured API endpoint such as http://localhost:3000/service/secured, it ask me to login with Keycloak login page and when logged-in, I have the following display on the screen:
Access Denied
on Node console I have the following message
Could not obtain grant code: Error: 400:Bad Request
and on Keycloak logs
type=CODE_TO_TOKEN_ERROR, realmId=xxxxxx, clientId=tst-api, userId=yyyyyy, ipAddress=172.17.0.1, error=invalid_code, grant_type=authorization_code, code_id=zzzzzz, client_auth_method=client-secret
I also tried to protect access to routes by using realm role instead of client roles (realm-tst-user, realm-tst-admin) but the result is the same...
I do not see what's wrong in my config, I tried also other tutorials but there's nothing different from what I did, any idea ?
on the other hand, how can I change the Access Denied HTML page displayed to the end user as it seems to be generated by Keycloak middleware ?

cookie session is not working on CodeSandbox and Repl?

I love coding on CodeSandbox for client and Repl for server.
I am learning create an auth microservices to handle twitter login recently.
I followed this tutorial
https://www.freecodecamp.org/news/how-to-set-up-twitter-oauth-using-passport-js-and-reactjs-9ffa6f49ef0/
and setup the client on CodeSandbox and Server on Repl
https://codesandbox.io/s/passport-pratice-twitter-p1ql3?file=/src/index.js
_handleSignInClick = () => {
// Authenticate using via passport api in the backend
// Open Twitter login page
// Upon successful login, a cookie session will be stored in the client
//let url = "https://Passport-pratice-twitter.chikarau.repl.co/auth/twitter";
let url = "http://localhost:4000/auth/twitter";
window.open(url, "_self");
};
https://repl.it/#chiKaRau/Passport-pratice-twitter#index.js
app.use(
cookieSession({
name: "session",
keys: [keys.COOKIE_KEY],
maxAge: 24 * 60 * 60 * 100
})
);
// parse cookies
app.use(cookieParser());
// initalize passport
app.use(passport.initialize());
// deserialize cookie from the browser
app.use(passport.session());
// set up cors to allow us to accept requests from our client
app.use(
cors({
//origin: "https://p1ql3.csb.app", // allow to server to accept request from different origin
origin: "http://localhost:3000",
methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
credentials: true // allow session cookie from browser to pass through
})
);
If I test them on localhost, they works perfectly (It displays login successfully)
However, testing them on CodeSandBox and Repl won't work because of req.user is undefined. The passport.session supposes to store the user session/cookie into req as req.user, and it will check whether the req.user exist and send a response back to client.
router.get("/login/success", (req, res) => {
if (req.user) {
res.json({
success: true,
message: "user has successfully authenticated",
user: req.user,
cookies: req.cookies
});
}
});
I also tested on both localhost and browser and set the appropriate domain
Client (PC) and Server (PC) - Working
Client (PC) and Server (REPL) - not Working
Client (CodeSandBox) and Server (PC) - not Working
Client (CodeSandBox) and Server (REPL) - not Working
My Question are why the cookie session is not working on online IDE such as CodeSandBox or Repl?
Is there any solution to get around this and run on CodeSandBox or Repl? If deploy both client and server on a server like heroku or digital ocean, would it gonna works as localhost?
Thanks

Why my GraphQL subscriptions are working on local server but not when deployed on live server

I am building a GraphQL server and have subscriptions in it. Once I deploy the server on my local machine and check the subscription in Playground it works, i.e. it listens to events and I get the newly added data. It means the subscriptions implementation is correct and there is nothing wrong with them. The subscriptions are working fine when I'm running on local (i.e localhost) but when I deploy the server on live (Heroku) it gives the below error when I listen to subscriptions in Playground:
{
"error": "Could not connect to websocket endpoint wss://foodesk.herokuapp.com/graphql. Please check if the endpoint url is correct."
}
Just for more information, my queries and mutations are also working on live server, it's just the subscriptions that are not working.
This is my code:
/**
* third party libraries
*/
const bodyParser = require('body-parser');
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const helmet = require('helmet');
const http = require('http');
const mapRoutes = require('express-routes-mapper');
const mkdirp = require('mkdirp');
const shortid = require('shortid');
/**
* server configuration
*/
const config = require('../config/');
const auth = require('./policies/auth.policy');
const dbService = require('./services/db.service');
const { schema } = require('./graphql');
// environment: development, testing, production
const environment = process.env.NODE_ENV;
const graphQLServer = new ApolloServer({
schema,
uploads: false
});
/**
* express application
*/
const api = express();
const server = http.createServer(api);
graphQLServer.installSubscriptionHandlers(server)
const mappedRoutes = mapRoutes(config.publicRoutes, 'api/controllers/');
const DB = dbService(environment, config.migrate).start();
// allow cross origin requests
// configure to allow only requests from certain origins
api.use(function (req, res, next) {
// Website you wish to allow to connect
res.setHeader('Access-Control-Allow-Origin', '*');
// Request methods you wish to allow
res.setHeader('Access-Control-Allow-Methods', '*');
// Request headers you wish to allow
res.setHeader('Access-Control-Allow-Headers', '*');
// Pass to next layer of middleware
next();
});
// secure express app
api.use(helmet({
dnsPrefetchControl: false,
frameguard: false,
ieNoOpen: false,
}));
// parsing the request bodys
api.use(bodyParser.urlencoded({ extended: false }));
api.use(bodyParser.json());
// public REST API
api.use('/rest', mappedRoutes);
// private GraphQL API
api.post('/graphql', (req, res, next) => auth(req, res, next));
graphQLServer.applyMiddleware({
app: api,
cors: {
origin: true,
credentials: true,
methods: ['POST'],
allowedHeaders: [
'X-Requested-With',
'X-HTTP-Method-Override',
'Content-Type',
'Accept',
'Authorization',
'Access-Control-Allow-Origin',
],
},
playground: {
settings: {
'editor.theme': 'light',
},
},
});
server.listen(config.port, () => {
if (environment !== 'production'
&& environment !== 'development'
&& environment !== 'testing'
) {
console.error(`NODE_ENV is set to ${environment}, but only production and development are valid.`);
process.exit(1);
}
return DB;
});
based on your error;
{
"error": "Could not connect to websocket endpoint wss://foodesk.herokuapp.com/graphql. Please check if the endpoint url is correct."
}
Your app is running on heroku app service i.e "herokuapp.com". as far as this service is concerned, you are not in control of the server configurations.
in development, you were serving your subscriptions under the "ws://" protocol which is converted to the secure version "wss://" when you deployed to heroku. the solution i have needs you to have access to the server via ssh. this means you upgrade your heroku subscription to have a vm with a dedicated ip address or any other cloud provider for that matter. if you have that in place do the following;
host your app on the vm and serve it, then edit the apache to proxy "yourdomain.com" to serve the app at maybe port 3000 as shown below
<VirtualHost *:80>
ServerName yourdomain.com
ProxyPreserveHost on
ProxyPass / http://localhost:3000/
ProxyPassReverse / http://localhost:3000/
RewriteEngine on
</VirtualHost>
Go to your domain registrar and add a subdomain through which you will be serving your subscriptions aka web-sockets.
this is done by adding a A record with the subdomain name eg "websocket" pointing to your vm ip address. this will make "websocket.yourdomain.com" available for use.
on your server install apache server and include a virtualhost as shown below
<VirtualHost *:80>
ServerName websocket.yourdomain.com
ProxyPreserveHost on
ProxyPass / ws://localhost:3000/
ProxyPassReverse / ws://localhost:3000/
RewriteEngine on
</VirtualHost>
restart apache server
at this point, your app is running on "yourdomain.com" which is where all your graphql mutations and queries are also served. but all the subscriptions will be established at "websocket.yourdomain.com" which will proxy all the requests to the "ws://" protocol hence reaching your subscription server without an error.
N/B Your client side code will use "wss://websocket.yourdomain.com" for subscription connections

redirect based authentication flow in secured by keycloak node.js app behind application gateway

I'm getting access denied errors in secured node.js app which is an official keycloak example app
Secured app was dockerized and put behind application gateway which is itself dockerized.
The application gateway is node.js express application which uses http/https packages and routes incoming traffic to node.js secured app.
So, to access app url mapped urls were added to the gateway:
mappings:
- /:/
- /login:/login
- /logout:/logout
- /protected/resource:/protected/resource
Gateway does ssl offloading.
Keycloak was dockerized too and its /auth endpoint was mapped inside the gateway.
The app code is below:
var Keycloak = require('keycloak-nodejs-connect');
var hogan = require('hogan-express');
var express = require('express');
var session = require('express-session');
var app = express();
var server = app.listen(3005, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
app.set('view engine', 'html');
app.set('views', require('path').join(__dirname, '/view'));
app.engine('html', hogan);
app.enable('trust proxy')
var memoryStore = new session.MemoryStore();
app.use(session({
secret: 'mySecret',
resave: false,
saveUninitialized: true,
store: memoryStore
}));
app.get('/', function (req, res) {
res.render('index');
});
var memoryStore = new session.MemoryStore();
app.use(session({
secret: 'mySecret',
resave: false,
saveUninitialized: true,
store: memoryStore
}));
// Additional configuration is read from keycloak.json file
// installed from the Keycloak web console.
var keycloak = new Keycloak({
store: memoryStore
});
app.use(keycloak.middleware({
logout: '/logout',
admin: '/',
protected: '/protected/resource'
}));
app.get('/login', keycloak.protect(), function (req, res) {
res.render('index', {
result: JSON.stringify(JSON.parse(req.session['keycloak-token']), null, 4),
event: '1. Authentication\n2. Login'
});
});
app.get('/protected/resource', keycloak.enforcer(['resource:view', 'resource:write'], {
resource_server_id: 'nodejs-apiserver'
}), function (req, res) {
res.render('index', {
result: JSON.stringify(JSON.parse(req.session['keycloak-token']), null, 4),
event: '1. Access granted to Default Resource\n'
});
});
keycloak.json is:
{
"realm" : "nodejs-example",
"realm-public-key" : "[public_key]",
"auth-server-url" : "https://[https://[gateway_url]]/auth",
"ssl-required" : "none",
"resource" : "nodejs-connect",
"public-client" : true
}
When https://[gateway_url]/ is accessed in the browser, KeyCloak redirects to login ui, user/password is entered in the login ui and after that access denied error is seen in the browser.
Below error is popped in the app logs:
Could not obtain grant code error: { Error: self signed certificate
in certificate chain
So basically the app fails to exchange authorization code for access token.
What i tried:
1) Accessing Keycloak token endpoint with curl as follows succeeds (Access/Refresh token is returned):
curl -k --key [keypath] --cert [certpath:passphrase] -d "grant_type=authorization_code&client_id=nodejs-connect&redirect_uri=https://[gw_url]/login?auth_callback=1&client_session_state=[client_state]&code=[authz_code]
-X POST 'https://[gw_url]/auth/realms/nodejs-example/protocol/openid-connect/token'
2) changing "auth-server-url" to "https://[gateway_url]:8080/auth" in keycloak.json helped too. Access token is returned. 8080 is published port of Keycloak docker container.
So, i guess the issue is that node.js adapter in the app doesn't present ssl ceritificate to gateway when it wants to replace the authz code with access token. So i tried to change auth-server-url to relative /auth. However
Could not obtain grant code error: { Error: connect ECONNREFUSED
127.0.0.1:80
is popped inside the logs of the app.
How to configure keycloak node.js adapter correctly to secure services behind the application gateway?
Hey I just had the same error and fixed by putting in the LAN ip Address in the keycloak.json instead of the host name.

Express, Create-React-App, & passport-twitter

I am setting up Twitter oauth on my create-react-app application by using a helper function (via axios) on the front end to initiate the passport oauth process on my backend. I am currently in development, so I am hosting my express server on port 3001 and my front end on port 3000 with a proxy on the front end to port 3001. I have set up CORS permissions via the cors npm package.
No matter what set of configurations I try, I am unable to complete the Twitter OAuth process. I have tried switching ports, keeping the same ports; I have tried proxying my backend with express-http-proxy.
I have used http://127.0.0.1 instead of localhost in both my callback function and my initial api call, trying both ports 3000 and 3001.
I'm at the point where I'm not sure where I'm going wrong or if I need to abandon the passport-twitter for other solutions.
In every case, I keep getting the following error:
Failed to load https://api.twitter.com/oauth/authenticate?
oauth_token=alphanumericcoderedactedherebyme: No 'Access-Control-Allow-Origin'
header is present on the requested resource. Origin 'http://localhost:3000' is
therefore not allowed access.
Depending on the configuration I have attempted, I get origin as null or http://localhost:3001 or http://127.0.0.1.
Note, I successfully call my backend api numerous times for other reasons, such as connecting to Yelp Fusion API. Moreover, I am using middleware to log my session data and I can see that I am successfully getting oauth_token and oauth_token_secret from Twitter. The call is failing on the next leg of the oauth process:
[0] *************SESSION MIDDLEWARE***************
[0] Session {
[0] cookie:
[0] { path: '/',
[0] _expires: 2018-01-06T20:20:31.913Z,
[0] originalMaxAge: 2678400000,
[0] httpOnly: true },
[0] 'oauth:twitter':
[0] { oauth_token: 'alphanumericcoderedactedherebyme',
[0] oauth_token_secret: 'alphanumericcoderedactedherebyme' } }
[0]
[0] Logged In:
[0] __________ false
[0] **********************************************
Here is relevant portions of my code -
BACKEND CODE
SERVER.JS
// Dependencies
const express = require("express");
const cors = require("cors");
const passport = require('passport');
// Initialize Express Server
const app = express();
// Specify the port.
var port = process.env.PORT || 3001;
app.set('port', port);
app.use(passport.initialize());
app.use(passport.session());
//enable CORS
app.use(cors());
//set up passport for user authentication
const passportConfig = require('./config/passport');
require("./controllers/auth-controller.js")(app);
// Listen on port 3000 or assigned port
const server = app.listen(app.get('port'), function() {
console.log(`App running on ${app.get('port')}`);
});
PASSPORT.JS
const passport = require('passport');
const TwitterStrategy = require('passport-twitter').Strategy;
passport.use(new TwitterStrategy({
consumerKey: process.env.TWITTER_CONSUMER_KEY,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
callbackURL: process.env.NODE_ENV === 'production' ? process.env.TWITTER_CALLBACK_URL : 'http://localhost:3000/auth/twitter/callback'
},
function(accessToken, refreshToken, profile, done) {
...etc, etc, etc
AUTH-CONTROLLER.JS
const router = require('express').Router();
const passport = require('passport');
module.exports = function(app) {
router.get('/twitter', passport.authenticate('twitter'));
router.get('/twitter/callback',
passport.authenticate('twitter', {
successRedirect: '/auth/twittersuccess',
failureRedirect: '/auth/twitterfail'
})
);
router.get('/twittersuccess', function(req, res) {
// Successful authentication
res.json({ user: req.user, isAuth: true });
})
router.get('/twitterfail', function(req, res) {
res.statusCode = 503;
res.json({ err: 'Unable to Validate User Credentials' })
})
app.use('/auth', router);
}
FRONTEND CODE
HELPERS.JS
import axios from 'axios';
export function authUser() {
return new Promise((resolve, reject) => {
axios.get('/auth/twitter', {
proxy: {
host: '127.0.0.1',
port: 3001
}
}).then(response => {
resolve(response.data);
}).catch(err => {
console.error({ twitterAuthErr: err })
if (err) reject(err);
else reject({ title: 'Error', message: 'Service Unavailable - Please try again later.' });
});
});
}
UPDATE
I verified that Passport Authentication works on my backend port. I called the endpoint directly in the browser and was redirected to the Twitter auth which then returned to my callback the new user saved in my schema which was saved to the session data.
This means the problem lies with the use of Create-React-App on a different port than my backend.
http://127.0.0.1:3001/auth/twittersuccess
"user": {
"_id": "redactedbyme",
"name": "Wesley L Handy",
"__v": 0,
"twitterId": "redactedbyme",
"favorites": [],
"friends": []
},
"isAuth": true
I could not find a solution to the problem at hand, after consulting several developers and posting this question in other forums.
However, according to this blog, passport-twitter is not optimized for RESTful apis. This same blog provides a helpful tutorial for using this passport-twitter-token strategy together with react-twitter-auth found here
The issue relates to the fact that with Create-React-App the application runs on two different servers, one for the front-end and another for the back. There is no way around the CORS issue without a series of communications between the front-end and back-end, which passport does not allow. Passport is a great tool for handling OAuth on a single-server, but there is quite a bit of back and forth needed in OAuth that requires more complexity.
The tutorial by Ivan Vasiljevic is a helpful starting place for both understanding and working through that complexity.
If anyone coming here got stuck with fetching users with Reactjs from passport-twitter strategy, Here is the solution I found.
All you have to do is allowing credentials
Enabling credentials gives the ability to use cookies or express-session on the frontend. Read MDN for more details.
Back-End:
// cors policy setup
app.use(
cors({
origin: "http://localhost:3000", // front end url
optionsSuccessStatus: 200,
credentials: true,
})
);
Front-End:
axios.get(`${apiURL}/profile`, { withCredentials: true })

Resources