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 ?
Related
I have a meteor/nodeJs app that needs to connect to my client to authentify. I set up a connection access point as such (I just anonymized the various values):
import Keycloak from "keycloak-connect";
import { WebApp } from "meteor/webapp";
import express from "express";
import session from "express-session";
const app = express();
const memoryStore = new session.MemoryStore();
app.use(
session({
secret: "secret",
resave: false,
saveUninitialized: true,
store: memoryStore,
})
);
const kcConfig = {
clientId: "clientId",
serverUrl: "realmUrl",
realm: "clientName",
realmPublicKey: "publicKey",
};
const keycloak = new Keycloak({ store: memoryStore }, kcConfig);
app.use(keycloak.middleware());
app.get("/connect", keycloak.protect(), (req, res) => {
// doing my stuff here
res.writeHead(301, {
Location: "/connected",
});
res.end();
});
WebApp.connectHandlers.use(app);
The problem is:
When I run my server locally and go to the /connect link, I am redirected to the connection platform. I connect and I am sent back to my localhost:3000/connected => Everything works as intended
when I do exactly the same flow on the production environment I am getting an access denied (blank page with only access denied written) after trying to login for the first time. If I then manually go back to the /connect link I am getting directly connected (I guess I got the token properly and could connect again)
I don't know why the behaviour is different on both environment and why I am getting an access denied page when in prod.
As mentioned in comments I had an issue with my ROOT_URL, a trailing slash was left. Went better after removing it.
This might seem like a redundant question, but please hear me out first:
I'm working with a React Frontend and a Node Backend. I'm using JWT to deal with user authentication. Right now, I'm having trouble actually working with the JWT and performing the authentication. Here's where I'm stuck:
~ I try setting the token as an http cookie in my backend. If i work with postman, I see the token being set. However, when I use req.cookies.token to try and receive the token cookie to perform validation in the backend, I get an undefined value. Am I supposed to be sending the cookie from the frontend to the backend somehow? I feel like this is the part that I am missing.
Please advise!
SO I can give you an alternative solution to handling session making use of express-session and connect-mongodb-session this has tend to been the popular and somewhat secure solution for server session handling
Firstly you will need the following packages
npm i express-session connect-mongodb-session or yarn add express-session connect-mongodb-session
Now that we have packages that we need to setup our mongoStore and express-session middleware:
//Code in server.js/index.js (Depending on your server entry point)
import expressSession from "express-session";
import MongoDBStore from "connect-mongodb-session";
import cors from "cors";
const mongoStore = MongoDBStore(expressSession);
const store = new mongoStore({
collection: "userSessions",
uri: process.env.mongoURI,
expires: 1000,
});
app.use(
expressSession({
name: "SESS_NAME",
secret: "SESS_SECRET",
store: store,
saveUninitialized: false,
resave: false,
cookie: {
sameSite: false,
secure: process.env.NODE_ENV === "production",
maxAge: 1000,
httpOnly: true,
},
})
);
Now the session middleware is ready but now you have to setup cors to accept your ReactApp so to pass down the cookie and have it set in there by server
//Still you index.js/server.js (Server entry point)
app.use(
cors({
origin: "http://localhost:3000",
methods: ["POST", "PUT", "GET", "OPTIONS", "HEAD"],
credentials: true,
})
);
Now our middlewares are all setup now lets look at your login route
router.post('/api/login', (req, res)=>{
//Do all your logic and now below is how you would send down the cooki
//Note that "user" is the retrieved user when you were validating in logic
// So now you want to add user info to cookie so to validate in future
const sessionUser = {
id: user._id,
username: user.username,
email: user.email,
};
//Saving the info req session and this will automatically save in your mongoDB as configured up in sever.js(Server entry point)
request.session.user = sessionUser;
//Now we send down the session cookie to client
response.send(request.session.sessionID);
})
Now our server is ready but now we have to fix how we make request in client so that this flow can work 100%:
Code below: React App where you handling logging in
//So you will have all your form logic and validation and below
//You will have a function that will send request to server
const login = () => {
const data = new FormData();
data.append("username", username);
data.append("password", password);
axios.post("http://localhost:5000/api/user-login", data, {
withCredentials: true, // Now this is was the missing piece in the client side
});
};
Now with all this you have now server sessions cookies as httpOnly
So I have an application that uses React in frontend and Express in backend. From an example I saw, I configured Keycloak in both of these sides. But I am trying to protect the "/" path with the realm role admin. Which means a user who doesn't have the role admin cannot acccess that route. However it doesn't work. I am thinking it's because frontend does the work (if you are a Keycloak user, you can access it wheter you have admin role or not) and backend doesn't get a chance.
In frontend index.js:
let initOptions = {
url: 'http://localhost:8080/auth', realm: '{realm_name}', clientId: '{client_id}', onLoad: 'login-required'
}
let keycloak = Keycloak(initOptions);
keycloak.init({ onLoad: initOptions.onLoad }).then(function(authenticated) {
ReactDOM.render(<SnackbarProvider anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}} maxSnack={3}><App /></SnackbarProvider>, document.getElementById('root'));
}).catch(function() {
alert('failed');
});
In backend app.js:
const Keycloak = require('keycloak-connect');
const session = require('express-session');
var memoryStore = new session.MemoryStore();
var keycloak = new Keycloak({ store: memoryStore });
app.use(session({
secret:'secret',
resave: false,
saveUninitialized: true,
store: memoryStore
}));
app.use(keycloak.middleware());
app.get('/', keycloak.protect('realm:admin'), function(req, res){
console.log("HERE");
});
Here console.log("HERE"); this doesn't write anything to the console. So I am guessing this app.get doesn't get executed.
I do not know how to configure Keycloak to both of them. Any help?
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');
});
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.