Search code examples
node.jstypescriptexpresskeycloak

Access denied in keycloak.protect() Express node js app


I'm trying to get user info from keycloak with keycloak.protect() middleware, but always get 403 "Access denied".

Keycloak version 24.0.2

I did my own realm and client

My keycloak.json

{
  "realm": "realm",
  "auth-server-url": "http://localhost:8080/",
  "bearerOnly": true,
  "ssl-required": "external",
  "resource": "tender",
  "verify-token-audience": true,
  "credentials": {
    "secret": "some secret"
  },
  "use-resource-role-mappings": true,
  "confidential-port": 0,
  "policy-enforcer": {
    "credentials": {}
  }
}

index.js

const app: Express = express();
app.use(cors());
app.use(express.json());

app.use(
  session({
    secret: "mySecret",
    resave: false,
    saveUninitialized: true,
    store: memoryStore,
  })
);

app.use(
  keycloak.middleware({
    logout: "/logout",
    admin: "/",
  })
);

app.use("/api/v1", router);

const port = process.env.PORT || 3000;

const start = async () => {
  try {
    await sequelize.authenticate();
    await sequelize.sync();
    app.listen(port, () => {
      console.log(`[server]: Server is running at http://localhost:${port}`);
    });
  } catch (error) {
    console.log(error);
  }
};

start();
router.post("/login", userController.login);
router.get("", keycloak.protect(), userController.get);

Login is working. I get tokens, but get route always returns "Access denied". What should i do in admin console to resolve this

I had tried to play with roles in admin console, but it didnt help


Solution

  • You need to enable and configure Authorization Services in Keycloak. This involves setting up policies, permissions, resources, and scopes.

    Resources: The things you want to protect, such as APIs, web pages, or any other resource. It is the target of API.

    Scopes: It is an action, like a read, write, or delete to performed on the resource.

    Permission: the association between resources and scopes. It decide to allow or not to access a resource.

    Policy: Define the conditions under which access to a resource is granted.

    Demo

    I will leverage keycloak-nodejs-connect example

    I will show username user is 403 error but username admin is not error Access granted to Default Resource.

    user enter image description here

    admin enter image description here

    There are many steps, Please follow me step by step.

    Step 1. launching Keycloak v24.2

    In here

    Step 2. Import nodejs-example realm

    {
        "realm": "nodejs-example",
        "enabled": true,
        "sslRequired": "external",
        "registrationAllowed": true,
        "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
        "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
        "requiredCredentials": [ "password" ],
        "users" : [
            {
                "username" : "user",
                "enabled": true,
                "email" : "sample-user@nodejs-example",
                "firstName": "Sample",
                "lastName": "User",
                "credentials" : [
                    { "type" : "password",
                      "value" : "password" }
                ],
                "realmRoles": [ "user" ],
                "clientRoles": {
                    "account": ["view-profile", "manage-account"]
                }
            }
        ],
        "roles" : {
            "realm" : [
                {
                    "name": "user",
                    "description": "User privileges"
                },
                {
                    "name": "admin",
                    "description": "Administrator privileges"
                }
            ]
        },
        "scopeMappings": [
            {
                "client": "nodejs-connect",
                "roles": ["user"]
            }
        ],
        "clients": [
            {
                "clientId": "nodejs-connect",
                "enabled": true,
                "publicClient": true,
                "baseUrl": "/",
                "adminUrl" : "http://localhost:3000/",
                "baseUrl" : "http://localhost:3000/",
                "redirectUris": [
                    "http://localhost:3000/*"
                ],
                "webOrigins": []
            },
            {
                "clientId": "nodejs-apiserver",
                "enabled": true,
                "secret": "secret",
                "redirectUris": [
                  "http://localhost:3000/*"
                ],
                "webOrigins": [
                  "http://localhost:3000/*"
                ],
                "serviceAccountsEnabled": true,
                "authorizationServicesEnabled": true,
                "authorizationSettings": {
                  "resources": [
                    {
                      "name": "resource",
                      "type": "urn:nodejs-apiserver:resources:default",
                      "ownerManagedAccess": false,
                      "uris": [
                        "/*"
                      ],
                      "scopes": [
                        {
                          "name": "view"
                        },
                        {
                          "name": "write"
                        }
                      ]
                    }
                  ]
                }
              }
        ]
    }
    

    enter image description here

    Result of nodejs-example realm import

    enter image description here

    In nodejs-apiserver client has default resource with write and view scopes.

    I will show this resource can be accessed by admin user.

    enter image description here

    Step 3. Add admin user

    password is 1234

    enter image description here

    Step 4. Set up policy

    enter image description here

    Result of setup policy

    enter image description here

    Step 5. Set up permission

    enter image description here

    Result of setup permission

    enter image description here

    Step 6. Setup Server

    File tree

    enter image description here

    Save as index.html under view directory

    <html>
    <head>
        <style>
            ul {
                list-style-type: none;
                margin: 0;
                padding: 0;
                background-color: #f1f1f1;
            }
    
            li a {
                display: block;
                color: #000;
                padding: 8px 0 8px 16px;
                text-decoration: none;
    
            }
    
            /* Change the link color on hover */
            li a:hover {
                background-color: #555;
                color: white;
            }
    
            .nav {
                float: left;
                width: 250px;
            }
            .content {
                margin-left: 270px;
    
            }
            pre {
                word-wrap: break-word;
                white-space: pre-wrap;
                background-color: #ddd;
                border: 1px solid #ccc;
                padding: 20px;
            }
        </style>
    </head>
    <body>
    
    <h1 style="width: 100%; text-align: center;">NodeJS Keycloak Example</h1>
    <hr/>
    
    <div class="nav">
        <ul>
            <li><a href="/login">Login</a></li>
            <li><a href="/protected/resource">Protected Resource</a></li>
            <li><a href="/logout">Logout</a></li>
        </ul>
    </div>
    <div class="content">
        <h2>Result</h2>
        <pre id="output">
    {{result}}
        </pre>
    
        <h2>Events</h2>
        <pre id="events">
    {{event}}
        </pre>
    </div>
    
    
    </body>
    </html>
    

    Save as server.js

    const Keycloak = require('keycloak-connect')
    const hogan = require('hogan-express')
    const express = require('express')
    const session = require('express-session')
    
    const app = express()
    
    const server = app.listen(3000, function () {
      const host = server.address().address
      const port = server.address().port
      console.log('Example app listening at http://%s:%s', host, port)
    })
    
    // Register '.mustache' extension with The Mustache Express
    app.set('view engine', 'html')
    app.set('views', require('path').join(__dirname, '/view'))
    app.engine('html', hogan)
    
    // A normal un-protected public URL.
    
    app.get('/', function (req, res) {
      res.render('index')
    })
    
    // Create a session-store to be used by both the express-session
    // middleware and the keycloak middleware.
    
    const memoryStore = new session.MemoryStore()
    
    app.use(session({
      secret: 'mySecret',
      resave: false,
      saveUninitialized: true,
      store: memoryStore
    }))
    
    // Provide the session store to the Keycloak so that sessions
    // can be invalidated from the Keycloak console callback.
    //
    // Additional configuration is read from keycloak.json file
    // installed from the Keycloak web console.
    
    const keycloak = new Keycloak({
      store: memoryStore
    })
    
    // Install the Keycloak middleware.
    //
    // Specifies that the user-accessible application URL to
    // logout should be mounted at /logout
    //
    // Specifies that Keycloak console callbacks should target the
    // root URL.  Various permutations, such as /k_logout will ultimately
    // be appended to the admin URL.
    
    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'
      })
    })
    

    package.json

    {
      "name": "nodejs-keycloak-example",
      "version": "0.1.0",
      "description": "Example page that demonstrates available keycloak functionality",
      "main": "index.js",
      "scripts": {
        "start": "node index.js"
      },
      "author": "Roman Jurkov <winfinit@gmail.com>",
      "license": "Apache-2.0",
      "dependencies": {
        "express": "^4.19.2",
        "express-session": "^1.18.0",
        "hogan-express": "^0.5.2",
        "keycloak-connect": "^24.0.2"
      }
    }
    

    Step 7. Install dependencies and run it

    npm install
    
    node index.js
    

    enter image description here

    Step 8. Open by Browser

    http://localhost:3000
    

    enter image description here

    Step 9. 403 Error for user

    Credential

    username: user
    password: password
    

    enter image description here

    Result

    enter image description here

    Step 10. Access Grant for admin

    Credential

    username: admin
    password: 1234
    

    enter image description here

    Result

    User admin can access resource without error

    enter image description here

    This code passed and displayed Access granted message in html

    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'
      })
    })
    

    Conclusion

    So your "tender" resource needs to set up a policy/permission for your specific user.