Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Keycloak Express Openid-client
Austin Cunningham
Austin Cunningham

Posted on • Edited on

     

Keycloak Express Openid-client

Keycloak isdeprecating their client adapters (keycloak-connect) for Node and recommending openid-client as a replacement.

Setup Keycloak

First Idownload keycloak extract it and you can run it with the following command

bin/kc.sh start-dev
Enter fullscreen modeExit fullscreen mode

You can then loginhttp://localhost:8080, first time you do keycloak asks you to set an admin user and password.

Create a Realm and give it an name and create it. I am using keycloak-express for my realm name
Create realm

The create a Client using openid-connect in the Realm
Create a client

Set the Valid Redirect URIs and select save,
set valid redirect URIs

NOTE:you can specify specific routes here but I am using a wild card(not recommend best practice)

Create a user its documentedhere so I won't go into it.

That's it for Keycloak setup

Setup Openid-client with Passport in Express

We are going to use thisopenid-client andpassport to connect to keycloak. I install the following

npminstallpassportnpminstallopenid-clientnpminstallexpress-sessionnpminstallexpress
Enter fullscreen modeExit fullscreen mode

From the Realm we need the openid-configuration can be got from an endpoint

/realms/{realm-name}/.well-known/openid-configuration
Enter fullscreen modeExit fullscreen mode

So in my case the realm name is keycloak-express so the url will behttp://localhost:8080/realms/keycloak-express/.well-known/openid-configuration the output is as follows
.well-known url output
All we need is thisissuer:"http://localhost:8080/realms/keycloak-express" url to connect openid-client to keycloak as follows

'use strict';importexpressfrom'express';import{Issuer,Strategy}from'openid-client';importpassportfrom'passport';importexpressSessionfrom'express-session';constapp=express();// use the issuer url hereconstkeycloakIssuer=awaitIssuer.discover('http://localhost:8080/realms/keycloak-express')// don't think I should be console.logging this but its only a demo app// nothing bad ever happens from following the docs :)console.log('Discovered issuer %s %O',keycloakIssuer.issuer,keycloakIssuer.metadata);// client_id and client_secret can be what ever you want// may be worth setting them up as env varsconstclient=newkeycloakIssuer.Client({client_id:'keycloak-express',client_secret:'long_secret-here',redirect_uris:['http://localhost:3000/auth/callback'],post_logout_redirect_uris:['http://localhost:3000/logout/callback'],response_types:['code'],});
Enter fullscreen modeExit fullscreen mode

I then setup express sessions

varmemoryStore=newexpressSession.MemoryStore();app.use(expressSession({secret:'another_long_secret',resave:false,saveUninitialized:true,store:memoryStore}));
Enter fullscreen modeExit fullscreen mode

Then setup passport to use open connect id strategy

app.use(passport.initialize());app.use(passport.authenticate('session'));// this creates the strategypassport.use('oidc',newStrategy({client},(tokenSet,userinfo,done)=>{returndone(null,tokenSet.claims());}))passport.serializeUser(function(user,done){done(null,user);});passport.deserializeUser(function(user,done){done(null,user);});
Enter fullscreen modeExit fullscreen mode

Most of above is copied from the passport docs, I foundthis blog helpful in explaining serialize/deserialize.

Next I setup the authentication route this makes use of the the callbackredirect_uris: from thekeycloakIssuer.Client

// default protected route /testapp.get('/test',(req,res,next)=>{passport.authenticate('oidc')(req,res,next);});// callback always routes to testapp.get('/auth/callback',(req,res,next)=>{passport.authenticate('oidc',{successRedirect:'/testauth',failureRedirect:'/'})(req,res,next);});
Enter fullscreen modeExit fullscreen mode

I then setup a function to check if a route is authenticated

// function to check weather user is authenticated, req.isAuthenticated is populated by password.js// use this function to protect all routesvarcheckAuthenticated=(req,res,next)=>{if(req.isAuthenticated()){returnnext()}res.redirect("/test")}
Enter fullscreen modeExit fullscreen mode

This can then be used on protected routes

app.get('/testauth',checkAuthenticated,(req,res)=>{res.render('test');});app.get('/other',checkAuthenticated,(req,res)=>{res.render('other');});//unprotected routeapp.get('/',function(req,res){res.render('index');});
Enter fullscreen modeExit fullscreen mode

Finally I set the logout route up this also uses a callbackpost_logout_redirect_uris from thekeycloakIssuer.Client

// start logout requestapp.get('/logout',(req,res)=>{res.redirect(client.endSessionUrl());});// logout callbackapp.get('/logout/callback',(req,res)=>{// clears the persisted user from the local storagereq.logout();// redirects the user to a public routeres.redirect('/');});
Enter fullscreen modeExit fullscreen mode

And set the app to listen

app.listen(3000,function(){console.log('Listening at http://localhost:3000');});
Enter fullscreen modeExit fullscreen mode

Repohere with some extra code around views. Looks like this

login flow

Top comments(28)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
darknessm0404 profile image
Micaël Félix
  • Joined

Just for information if you follow this but have no render feature installed in express, it will fail with "Cannot GET /testauth" even if it's connected.
Just useres.send('You are connected'); instead ofres.render('test'); for the /testauth route.

CollapseExpand
 
keztur profile image
Kez
  • Work
    GUTcert Berlin
  • Joined

Since Version 0.6.0 of passport the "logout" method is asynchronous. The "logout callback" must therefore be changed to.

// logout callbackapp.get('/logout/callback', (req, res, next) => {    req.logout((err) => {        if (err) { return next(err) }        res.redirect('/');    });      });
Enter fullscreen modeExit fullscreen mode

Maybe you can update your tutorial accordingly. (Your article was a great help!)

CollapseExpand
 
keztur profile image
Kez
  • Work
    GUTcert Berlin
  • Joined

While trying to connect a vue application to keycloak using your code I noticed that there seems to be no further communication between keycloak and nodejs after a successful login. Once a session is created the validity of the connection between browser and nodejs is based on the validity of the session cookie alone.
In other words: If I end the session in keycloak by other means (e.g. logout by another client or by admin) then the express application won't notice.
Is this intended or did I make a mistake?
It looks to me like you switch from an oidc authentication to a http-cookie authentication.
Shouldn't there be a check in regular intervalls whether the user is still logged-in in keycloak? Or do you have an idea how this could beachived?

CollapseExpand
 
austincunningham profile image
Austin Cunningham
nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined

Hmm yea interesting problem, I didn't factor in logging out by keycloak admin or by another client my logic is only triggered when you hit the endpoint in express. Here is another example using bearer which might be better suited to your use casegithub.com/keycloak/keycloak/discu.... A regular check sounds like the way I would go , I don't believe that express-sessions has an event emitted on session expired. Without digging to much into it I would create another endpoint/check-session and poll it using fetch something like

setInterval(() => {  fetch('/check-session')    .then(response => response.json())    .then(data => {      // Handle the received data from the server      console.log(data);    })    .catch(error => {      // Handle any errors that occur during the request      console.error(error);    });}, 5000); // Poll every 5 seconds
Enter fullscreen modeExit fullscreen mode

I haven't tested it but it should probably work.

CollapseExpand
 
keztur profile image
Kez
  • Work
    GUTcert Berlin
  • Joined
• Edited on• Edited

Thank you. But I finally decided to go for a different server technology. I never used NodeJS in a production environment before and I was rather shocked as I saw the long "todo list" for doing so. I don't feel inclined to study the 25th "library", "adapter", "plug-in" or whatever is necessary just to store session cookies.
So I came back to my good old PHP/Apache backend using "jumbojett" as on OIDC adapter.
However - I just wanted to let you know that I also rely on http sessions. But what I do to keep the connection to keycloak is: I store theaccess token in asession variable (in the backend) and with each AJAX request (it's a Vue SPA) I send the access token to keycloak to verify whether the user is still authorized.
So there are actually two sessions running (just like in your example), one in PHP/Apache and one in keycloak. And I need to make sure that the PHP session is at least as long (time-wise) as the keycloak session.
I believe this is something which would work in your example too.

CollapseExpand
 
devtorello profile image
Tatiana Vitorello
daydreaming since 1998. back-end addict and in love with deno. :)
  • Location
    São Paulo, SP
  • Education
    Faculdade de Tecnologia de São Paulo
  • Pronouns
    She / Her
  • Work
    Software Engineer @ Nomo
  • Joined

Hey, Austin! Thank you so much for your article, I was able to make it work locally with it! :D

However, I'm facing some issues to run my application on docker. When I try to run both Keycloak and the application on containers, I receive the following error:

Error: connect ECONNREFUSED 127.0.0.1:8080users-app  |     at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1195:16) {users-app  |   errno: -111,users-app  |   code: 'ECONNREFUSED',users-app  |   syscall: 'connect',users-app  |   address: '127.0.0.1',users-app  |   port: 8080users-app  | }
Enter fullscreen modeExit fullscreen mode

I'm running keycloak through docker-compose on port 8080 and usinghttp://localhost:8080/auth/realms/my-realm/.well-known/openid-configuration onIssuer.discover. When trying to access this link through the browser, it works normally.

Do you have some hint on what may be causing it? I tried to tie a bridge network between keycloak container and nodejs container and it did not work also.

CollapseExpand
 
austincunningham profile image
Austin Cunningham
nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined
• Edited on• Edited

Thanks for taking the time to try this out in a docker container. So the issue is this linegithub.com/austincunningham/keyclo... where the localhost is the localhost inside the container and has no visibility on the global localhost (if that makes sense). Some solutions herehow-to-connect-to-localhost-within... , I tried

ip addr show docker0
Enter fullscreen modeExit fullscreen mode

to get the ip address and use that instead of localhost in the code and rebuilt the container

const keycloakIssuer = await Issuer.discover('http://172.17.0.1:8080/realms/keycloak-express')
Enter fullscreen modeExit fullscreen mode

Looks to be working

docker run -p 3000:3000 quay.io/austincunningham/keycloak-express-openid-client:latestDiscovered issuer http://172.17.0.1:8080/realms/keycloak-express {  claim_types_supported: [ 'normal' ],  claims_parameter_supported: true,  grant_types_supported: [    'authorization_code',    'implicit',    'refresh_token',    'password',    'client_credentials',    'urn:ietf:params:oauth:grant-type:device_code',    'urn:openid:params:grant-type:ciba'  ],...
Enter fullscreen modeExit fullscreen mode
CollapseExpand
 
austincunningham profile image
Austin Cunningham
nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined

There has to be a better way to get the docker0 ip address. This will get it

ifconfig |awk'/docker0/{getline; print}' |awk'{ print $2 }'
Enter fullscreen modeExit fullscreen mode

You can then create an environment variable

exportDOCKERHOST=$(ifconfig |awk'/docker0/{getline; print}' |awk'{ print $2 }')
Enter fullscreen modeExit fullscreen mode

Change the issuer to use the environment variable

constkeycloakIssuer=awaitIssuer.discover("http://"+process.env.DOCKERHOST+":8080/realms/keycloak-express")
Enter fullscreen modeExit fullscreen mode

Can pass it in on docker run

docker run-p 3000:3000-eDOCKERHOST=$DOCKERHOST quay.io/austincunningham/keycloak-express-openid-client:latest
Enter fullscreen modeExit fullscreen mode

For docker compose you can use aenv file to pass in environment variables

Thread Thread
 
devtorello profile image
Tatiana Vitorello
daydreaming since 1998. back-end addict and in love with deno. :)
  • Location
    São Paulo, SP
  • Education
    Faculdade de Tecnologia de São Paulo
  • Pronouns
    She / Her
  • Work
    Software Engineer @ Nomo
  • Joined

Hey, Austin! Sorry for taking so long to reply, but I wanted to thank you for your help! It really helped me and worked like a charm! :)

CollapseExpand
 
devtorello profile image
Tatiana Vitorello
daydreaming since 1998. back-end addict and in love with deno. :)
  • Location
    São Paulo, SP
  • Education
    Faculdade de Tecnologia de São Paulo
  • Pronouns
    She / Her
  • Work
    Software Engineer @ Nomo
  • Joined

Also, here's how I configured my apps ondocker-compose.yml:

Network:

networks:  app-tier:    driver: bridge
Enter fullscreen modeExit fullscreen mode

Keycloak:

keycloak:    container_name: keycloak    image: jboss/keycloak:latest    restart: always    environment:      DB_VENDOR: POSTGRES      DB_ADDR: postgres      DB_DATABASE: postgres      DB_USER: postgres      DB_SCHEMA: public      DB_PASSWORD: password      KEYCLOAK_USER: admin      KEYCLOAK_PASSWORD: admin      JDBC_PARAMS: "useSSL=false"    ports:      - 8080:8080    depends_on:      postgres:        condition: service_healthy    networks:      - app-tier
Enter fullscreen modeExit fullscreen mode

And Node app:

users-app:    container_name: users-app    build:       context: .      dockerfile: ./server/users/Dockerfile    restart: always    ports:      - 8880:8880    environment:      - DOPPLER_TOKEN    networks:      - app-tier
Enter fullscreen modeExit fullscreen mode
CollapseExpand
 
nikunjm10 profile image
nikunjm10
  • Joined

Hello Austin,
I was using keycloak-connect till now and it was working fine for me. As this adapter is getting deprecated I will try your solution for open-id client adapter, But I have a few questions regarding it.
Can we do role based and attributed encryption using it?
Can we protect the resources using this adapter?
As you will know that in the keycloak.protect() we can pass the roles and attributes. Also for protecting resources we can use keycloak.enforce().

Also we can pass the configuration to the keycloak using the previous adapter, can we do it with the help of this?

Thanks & Regards
Nikunj Mangla

CollapseExpand
 
austincunningham profile image
Austin Cunningham
nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined

Hi Nikunj,
Don't get me wrong here I lovedkeycloak-connect, it offered so much more out of the box thatOpenID client does. OpenID client doesn't have this functionally as it is a generic client to be used with any OIDC provider. I am not sure without further investigation weather it is possible to pass roles within the auth flow or weather its possible to do something similar tokeycloak.enforce() on resources, as for passing configuration I suspect that this functionality isn't available via OpenID client. These a all good questions It might be worth raising these with the Keycloak teamgithub.com/keycloak/keycloak/discu... around the deprecation of the Node adapter.

Regards,
Austin

CollapseExpand
 
austincunningham profile image
Austin Cunningham
nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined

Hi Ninkunj,

I was just reading down through the github discussions and looks like roles is possible seegithub.com/keycloak/keycloak/discu....
Regards,
Austin

CollapseExpand
 
nikunjm10 profile image
nikunjm10
  • Joined

Hello Austin,

Thank you for replying to my queries. I have also posted my concerns on the github discussion page. I will try with the roles checking through the link you provided. I am trying another module for now which is @keycloak/keycloak-admin-client not sure though if it will work or not for me. Let's see when they are going to provide a good support for the keycloak on nodejs

Thread Thread
 
kasir-barati profile image
Mohammad Jawad (Kasir) Barati
Love to work with cutting edge technologies and on my journey to learn and teach. Having a can-do attitude and being industrious are the reasons why I question the status quo an venture in the unknown

Did you manage to do cool stuff like role checking and resource checking with openid-client or any other 3rd party lib. I was trying to achieve the same goal with openid-client but I stuck. Now I am wondering how to check roles, resources, ...

Any update@austincunningham,@nikunjm10 ?

Thread Thread
 
austincunningham profile image
Austin Cunningham
nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined

It was possible to do rolesgithub.com/keycloak/keycloak/discu... there is an example in the discussion. It looks likekeycloak-connect will be around for another whilegithub.com/keycloak/keycloak/discu...

CollapseExpand
 
pspaulding profile image
pspaulding
  • Location
    Grand Rapids, MI, USA
  • Education
    University of Michigan, Ann Arbor
  • Joined

When using keycloak-connect, a "kauth" object was available on the request object that included all sorts of useful information, including any custom user attributes that were mapped to the access token (kauth.grant.access_token). What would be the equivalent when using passport/openid?

CollapseExpand
 
austincunningham profile image
Austin Cunningham
nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined

I haven't usedkauth before but can see how it would be useful. I had a look at the request object returned by passport/openid I couldn't see an equivalent togrant.access_token in the request. So as far as I can see there is no equivalent tokauth.grant.access_token.

CollapseExpand
 
rubybornsinner profile image
Ruben Martins
  • Education
    Universidade Tecnica do Atlantico
  • Work
    Developer at DevGo
  • Joined

HI Austin! I am new to using keycloak, I am a bit confused on how to integrate this solution with a react application that is also secured by keycloak, can you lead me in the right path :)

CollapseExpand
 
austincunningham profile image
Austin Cunningham
nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined

This is not something I have done myself but I think this might cover it for youmedium.com/devops-dudes/secure-fro...

CollapseExpand
 
rubybornsinner profile image
Ruben Martins
  • Education
    Universidade Tecnica do Atlantico
  • Work
    Developer at DevGo
  • Joined

Thanks for your reply, I´m doing most exactly like the article you send me but in this case for backend I´m not using the keycloak node adapter but the Keycloak Express Openid-client that is in your article. Right now I´m getting a valid token in my react app but when I´m sending this token in the headers of my requets and use the function that you created to check authentication I´m still getting that I´m not authenticated.
Do I have to trigger the route /test from the react app to set the req.isAuthenticate to true or I just have to check if the token I send it´s valid ?

Thread Thread
 
austincunningham profile image
Austin Cunningham
nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined

Hey Ruben, as I've said its not something I have done so not too sure what advice I should give. I am curiousreq.isAuthenticated() should check for a valid token . It's something I would like to investigate when I have the time. If I figure it out I will post another blog.

CollapseExpand
 
kasir-barati profile image
Mohammad Jawad (Kasir) Barati
Love to work with cutting edge technologies and on my journey to learn and teach. Having a can-do attitude and being industrious are the reasons why I question the status quo an venture in the unknown

Hey, look at my repo:github.com/kasir-barati/you-say

I have a NestJS, and react app.

CollapseExpand
 
daniel1004k profile image
Daniel Kuruch
  • Joined

Hey, Austin ! Thank you for your article. But do you have such solution for Fastify ?

CollapseExpand
 
austincunningham profile image
Austin Cunningham
nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined

Hi Daniel, I haven't triedFastify, but is now on my todo list. I will stick a blog up if I get it working.

CollapseExpand
 
pspaulding profile image
pspaulding
  • Location
    Grand Rapids, MI, USA
  • Education
    University of Michigan, Ann Arbor
  • Joined

Thank you!

CollapseExpand
 
davidjlion profile image
DavidJLion
  • Joined

Hi Austin. Thanks for your article.
However when I run the application I am getting this error.
Image description

CollapseExpand
 
austincunningham profile image
Austin Cunningham
nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined

Hi David I can reproduce your error with the Keycloak server not running so you may not be running it on the default port

npm start> keycloak-express-openid-client@1.0.0 start> node index.jsnode:internal/process/esm_loader:94    internalBinding('errors').triggerUncaughtException(                              ^Error: connect ECONNREFUSED 127.0.0.1:8080    at TCPConnectWrap.afterConnect[as oncomplete](node:net:1187:16){  errno:-111,  code:'ECONNREFUSED',  syscall:'connect',  address:'127.0.0.1',  port: 8080}npm notice npm notice New major version of npm available! 8.11.0 -> 9.6.1npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.6.1npm notice Run npminstall-g npm@9.6.1 to update!npm notice
Enter fullscreen modeExit fullscreen mode

This makes sense as Keycloak's default port is 8080
Started up the keycloak server and was able to get it running

 npm start> keycloak-express-openid-client@1.0.0 start> node index.jsDiscovered issuer http://localhost:8080/realms/keycloak-express{  claim_types_supported:['normal'],  claims_parameter_supported:true,  grant_types_supported:['authorization_code','implicit','refresh_token','password','client_credentials','urn:ietf:params:oauth:grant-type:device_code','urn:openid:params:grant-type:ciba'],  ...  require_pushed_authorization_requests:false,  pushed_authorization_request_endpoint:'http://localhost:8080/realms/keycloak-express/protocol/openid-connect/ext/par/request',  mtls_endpoint_aliases:{    token_endpoint:'http://localhost:8080/realms/keycloak-express/protocol/openid-connect/token',    revocation_endpoint:'http://localhost:8080/realms/keycloak-express/protocol/openid-connect/revoke',    introspection_endpoint:'http://localhost:8080/realms/keycloak-express/protocol/openid-connect/token/introspect',    device_authorization_endpoint:'http://localhost:8080/realms/keycloak-express/protocol/openid-connect/auth/device',    registration_endpoint:'http://localhost:8080/realms/keycloak-express/clients-registrations/openid-connect',    userinfo_endpoint:'http://localhost:8080/realms/keycloak-express/protocol/openid-connect/userinfo',    pushed_authorization_request_endpoint:'http://localhost:8080/realms/keycloak-express/protocol/openid-connect/ext/par/request',    backchannel_authentication_endpoint:'http://localhost:8080/realms/keycloak-express/protocol/openid-connect/ext/ciba/auth'}}Listening at http://localhost:3000
Enter fullscreen modeExit fullscreen mode

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

nuff said
  • Location
    Waterford, Ireland
  • Work
    Senior Software Engineer at Red Hat
  • Joined

More fromAustin Cunningham

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp