Keycloak as SSO

By 增廣建文

What's Keycloak?

是一個單點登入(Single Sign On, SSO)系統服務,主要由redhat負責開發與維護
可以協助你驗證身份與管理會話(Session),也能串接其他平台做登入像是Google, FB等
支援多個標準協議,包含 OAuth 2.0 、 OpenID 和 saml

SSO Flow


也能夠和前端的React.js做串接,但這裡我們先用比較常見的flow來串後端

Install Keycloak

最簡單的安裝方式莫過於直接使用docker來啟動

docker run -d -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:17.0.0 start-dev

安裝完成後要去admin console設定一下使用者的帳號

http://localhost:8080/admin

Setup Realm and Add User

可以先在預設叫做master的realm下建立新的使用者

Setup User Credential

建議關掉Temporary的選項,這樣可以避免第一次登入就要求使用者更新密碼

Account Console

如果想要進一步的去設定使用者資訊的話可以到account console去

http://localhost:8080/realms/master/account/

Allow Apps to Login with User


要透過設定client才能去允許不同的連線方式



記得要設定Valid Redirect URIs不然之後會出錯,加上底下兩個

  • http://localhost:3000
  • https://www.keycloak.org/*

Verify Keycloak Setting

redhat提供的測試網頁讓你知道是不是有正確的設定好keycloak user帳號

https://www.keycloak.org/app/



如果測試的頁面會出現CORS error就要去client的Web Origins加上*

List all Endpoints

http://localhost:8080/realms/{realm-name}/.well-known/openid-configuration來查

Demo Client - Express


Keycloak官方有提供一些小範例專案可以去測試剛架的keycloak server
不論是Java或是Node.js版都會用到keycloak提供的SDK

const express = require('express');
const session = require('express-session');
const Keycloak = require('keycloak-connect');

const app = express();
const memoryStore = new session.MemoryStore();
const keycloak = new Keycloak({
  store: memoryStore
});
app.get('/service/admin', keycloak.protect('realm:admin'), function (req, res) {
  res.json({message: 'admin'});
});
app.listen(3000, function () {
  console.log('Started at port 3000');
});

Demo Client - Keycloak.json

keycloak-connect吃的設定都要放在keycloak.json

{
  "realm": "quickstart",
  "bearer-only": true,
  "auth-server-url": "http://localhost:8180/auth",
  "ssl-required": "external",
  "resource": "service-nodejs"
}

Test Login with Postman/ Thunder Client

用Postman來獲取之後call其他endpoint會需要的token

Decode Token with JWT

可以到JWT的官網去查看拿到的token意思

Use OAuth2 with Postman/ Thunder Client


可以透過OAuth自動產生token,就不用先去POST token endpoint

Setup OAuth Callback

Postman和Thunder Client使用OAuth時都得去client的validate redirect uri設定

  • Thunder Client: https://www.thunderclient.com/oauth/callback
  • Postman: https://oauth.pstmn.io/v1/callback

Other Endpoints

  • Auth: 用瀏覽器登入時會跳轉到的,會先去檢查request有沒有帶合法的cookie
  • Userinfo: 可以去查看目前登入的使用者資訊
  • Logout: Keycloak要登出還需要把server上的session給清掉

Thunder Client OAuth Fetch Userinfo

由於剛剛設定的OAuth會自動記住token,所以call其他受保護的endpoint就不會報錯

Keycloak + Express Full Guide

先pass

Fastify-Keycloak-Adapter

由於實作上處理Keycloak給的資訊會有些複雜,我們可以透過plugin快速的完成登入驗證
安裝方式是npm i fastify-keycloak-adapter

// server.js
import fastify from 'fastify'
import keycloak, { KeycloakOptions } from 'fastify-keycloak-adapter'

const server = fastify()

const opts = {
  appOrigin: 'http://localhost:3000',  // fastify server ip and port
  keycloakSubdomain: 'localhost:8080/realms/master',
  clientId: 'myclient',
//   clientSecret: 'client01secret'
}

server.register(keycloak, opts)

Downgrade Keycloak to v15

有一點要注意的是等等要用的fastify plugin官方範例是使用舊版URL
所以我們要重開一台fastify server並用較舊的版本

docker run -d -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:15.0.0

舊版的url會長的類似
http://localhost:8080/auth/realms/{realm-name}/.well-known/openid-configuration

Old Example

官方的舊版範例

// server.js
import fastify from 'fastify'
import keycloak, { KeycloakOptions } from 'fastify-keycloak-adapter'

const server = fastify()

const opts = {
  appOrigin: 'http://localhost:3000',
  keycloakSubdomain: 'localhost:8080/auth/realms/master',
  clientId: 'myclient',
//   clientSecret: 'client01secret'
}

server.register(keycloak, opts)

Client Secret

要將Access Type改成confidential才會有client secret的欄位

Login and Logout

// return user info
server.get('/users/me', async (request, reply) => {
  const user = request.session.user
  return reply.status(200).send({ user })
})

logout已經被寫在plugin內,只要callhttp://localhost:3000/logout就好

Connect Keycloak to React Project


這邊主要是讓react在initial render時就去call受到keycloak保護的endpoint
實際上當我們call受到保護但沒登入的endpoint時,就會被跳轉到keycloak的登入頁面了
其實Keycloak的SSO是支援跨subdomain去做登入的,但是為了避免遇到CORS的問題
會建議build好react專案後去給fastify serve

Fastify Serve Static

要先安裝套件npm i @fastify/static
還要透過npm run build去react專案下產生靜態的網頁

const path = require('path')

fastify.register(require('@fastify/static'), {
  root: path.join(__dirname, 'public'),
  prefix: '/public', // optional: default '/'
})

fastify.get('/public', function(req, rep){
    rep.sendFile('index.html')
})

如果prefix有設定的話,AppOrigin和react的package.json中的homepage也得跟著異動

Ignore manifest.json CORS error

目前在console還是會看到error,可以去修改public/index.html

<link rel="manifest" href="%PUBLIC_URL%/manifest.json" crossorigin="use-credentials" />

manifest link最後面的地方加上crossorigin="use-credentials"

或是可以去裝Allow CORS的chrome插件來自動隱藏

Alternatives

如果前後端真的要用不同port或許可以考慮分開使用client
或是用nginx強制keycloak server的response會多出對的cors header
fetch前先套上proxy也是可能的解法之一

附錄

Export Realm Settings

docker exec -it kc /opt/keycloak/bin/kc.sh export --realm master \
--file /tmp/master.json

指令中的--dir--file必須要擇一,也可以使用GUI來輸出

Import Realm Setting

docker exec -it kc bin/kc.sh import --file

Cookie跨subdomain

前後端port導致keycloak CORS block

Workround - 分開保護前後端