├── frontend ├── Dockerfile ├── src │ ├── keycloak.php │ ├── index.php │ ├── styles.css │ └── app.js ├── frontend-ingress.yaml └── frontend.yaml ├── backend ├── Dockerfile ├── src │ ├── keycloak.json │ ├── package.json │ └── app.js ├── backend-ingress.yaml └── backend.yaml ├── keycloak ├── keycloak-ingress.yaml ├── keycloak.yaml └── realm.json └── README.md /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.0-apache 2 | COPY src/ /var/www/html/ 3 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:9 2 | 3 | EXPOSE 8080 4 | 5 | WORKDIR /home/node/app 6 | 7 | COPY src/ /home/node/app 8 | 9 | RUN npm install 10 | 11 | CMD npm start 12 | -------------------------------------------------------------------------------- /backend/src/keycloak.json: -------------------------------------------------------------------------------- 1 | { 2 | "realm": "demo", 3 | "bearer-only": true, 4 | "auth-server-url": "${env.KEYCLOAK_URL}", 5 | "ssl-required": "external", 6 | "resource": "service-nodejs" 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/keycloak.php: -------------------------------------------------------------------------------- 1 | 'demo', 6 | 'auth-server-url' => $_ENV['KEYCLOAK_URL'], 7 | 'resource' => "app" 8 | ); 9 | 10 | echo json_encode($config, JSON_UNESCAPED_SLASHES); 11 | ?> 12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/backend-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: backend 5 | spec: 6 | tls: 7 | - hosts: 8 | - BACKEND_HOST 9 | rules: 10 | - host: BACKEND_HOST 11 | http: 12 | paths: 13 | - backend: 14 | serviceName: backend 15 | servicePort: 8080 16 | 17 | -------------------------------------------------------------------------------- /frontend/frontend-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: frontend 5 | spec: 6 | tls: 7 | - hosts: 8 | - FRONTEND_HOST 9 | rules: 10 | - host: FRONTEND_HOST 11 | http: 12 | paths: 13 | - backend: 14 | serviceName: frontend 15 | servicePort: 80 16 | 17 | -------------------------------------------------------------------------------- /keycloak/keycloak-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: keycloak 5 | spec: 6 | tls: 7 | - hosts: 8 | - KEYCLOAK_HOST 9 | rules: 10 | - host: KEYCLOAK_HOST 11 | http: 12 | paths: 13 | - backend: 14 | serviceName: keycloak 15 | servicePort: 8080 16 | 17 | -------------------------------------------------------------------------------- /backend/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "service-nodejs", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "start": "node app.js" 6 | }, 7 | "dependencies": { 8 | "keycloak-connect": "4.0.0-beta.3", 9 | "body-parser": "^1.13.3", 10 | "express": "^4.13.3", 11 | "express-session": "^1.14.2", 12 | "cors": "^2.8.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/backend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: backend 5 | labels: 6 | app: backend 7 | spec: 8 | ports: 9 | - name: http 10 | port: 8080 11 | targetPort: 8080 12 | selector: 13 | app: backend 14 | type: LoadBalancer 15 | --- 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: backend 20 | namespace: default 21 | spec: 22 | replicas: 1 23 | selector: 24 | matchLabels: 25 | app: backend 26 | template: 27 | metadata: 28 | labels: 29 | app: backend 30 | spec: 31 | containers: 32 | - name: backend 33 | image: kube-demo-backend 34 | imagePullPolicy: Never 35 | env: 36 | - name: KEYCLOAK_URL 37 | value: https://KEYCLOAK_HOST/auth 38 | ports: 39 | - name: http 40 | containerPort: 8080 41 | readinessProbe: 42 | httpGet: 43 | path: /public 44 | port: 8080 45 | -------------------------------------------------------------------------------- /frontend/frontend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: frontend 5 | labels: 6 | app: frontend 7 | spec: 8 | ports: 9 | - name: http 10 | port: 80 11 | targetPort: 80 12 | selector: 13 | app: frontend 14 | type: LoadBalancer 15 | --- 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: frontend 20 | namespace: default 21 | spec: 22 | replicas: 1 23 | selector: 24 | matchLabels: 25 | app: frontend 26 | template: 27 | metadata: 28 | labels: 29 | app: frontend 30 | spec: 31 | containers: 32 | - name: frontend 33 | image: kube-demo-frontend 34 | imagePullPolicy: Never 35 | env: 36 | - name: KEYCLOAK_URL 37 | value: https://KEYCLOAK_HOST/auth 38 | - name: SERVICE_URL 39 | value: https://BACKEND_HOST 40 | ports: 41 | - name: http 42 | containerPort: 80 43 | readinessProbe: 44 | httpGet: 45 | path: / 46 | port: 80 47 | -------------------------------------------------------------------------------- /keycloak/keycloak.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: keycloak 5 | labels: 6 | app: keycloak 7 | spec: 8 | ports: 9 | - name: http 10 | port: 8080 11 | targetPort: 8080 12 | selector: 13 | app: keycloak 14 | type: LoadBalancer 15 | --- 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: keycloak 20 | namespace: default 21 | spec: 22 | replicas: 1 23 | selector: 24 | matchLabels: 25 | app: keycloak 26 | template: 27 | metadata: 28 | labels: 29 | app: keycloak 30 | spec: 31 | containers: 32 | - name: keycloak 33 | image: jboss/keycloak 34 | env: 35 | - name: KEYCLOAK_USER 36 | value: "admin" 37 | - name: KEYCLOAK_PASSWORD 38 | value: "admin" 39 | - name: PROXY_ADDRESS_FORWARDING 40 | value: "true" 41 | ports: 42 | - name: http 43 | containerPort: 8080 44 | - name: https 45 | containerPort: 8443 46 | readinessProbe: 47 | httpGet: 48 | path: /auth/realms/master 49 | port: 8080 50 | -------------------------------------------------------------------------------- /keycloak/realm.json: -------------------------------------------------------------------------------- 1 | { 2 | "realm": "demo", 3 | "enabled": true, 4 | "users": [ 5 | { 6 | "username": "stian", 7 | "enabled": true, 8 | "credentials": [ 9 | { 10 | "type": "password", 11 | "value": "pass" 12 | } 13 | ], 14 | "realmRoles": [ 15 | "user" 16 | ], 17 | "clientRoles": { 18 | "account": [ 19 | "manage-account" 20 | ] 21 | } 22 | } 23 | ], 24 | "roles": { 25 | "realm": [ 26 | { 27 | "name": "user", 28 | "description": "User privileges" 29 | }, 30 | { 31 | "name": "admin", 32 | "description": "Administrator privileges" 33 | } 34 | ] 35 | }, 36 | "defaultRoles": [ 37 | "user" 38 | ], 39 | "clients": [ 40 | { 41 | "clientId": "app", 42 | "enabled": true, 43 | "publicClient": true, 44 | "redirectUris": [ 45 | "*" 46 | ], 47 | "webOrigins": [ 48 | "*" 49 | ] 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keycloak Kubernetes Demo 2 | 3 | This demo assumes you have minikube installed with the ingress addon enabled. 4 | 5 | ## Setup URLs 6 | 7 | The following URLs uses nip.io to prevent having to modify `/etc/hosts`. 8 | 9 | export MINIKUBE_IP=`minikube ip` 10 | export KEYCLOAK_HOST=keycloak.$MINIKUBE_IP.nip.io 11 | export BACKEND_HOST=backend.$MINIKUBE_IP.nip.io 12 | export FRONTEND_HOST=frontend.$MINIKUBE_IP.nip.io 13 | 14 | ## Keycloak 15 | 16 | kubectl create -f keycloak/keycloak.yaml 17 | 18 | cat keycloak/keycloak-ingress.yaml | sed "s/KEYCLOAK_HOST/$KEYCLOAK_HOST/" \ 19 | | kubectl create -f - 20 | 21 | echo https://$KEYCLOAK_HOST 22 | 23 | The Keycloak admin console should now be opened in your browser. Ignore the warning caused by the self-signed certificate. Login with admin/admin. Create a new realm and import `keycloak/realm.json`. 24 | 25 | The client config for the frontend allows any redirect-uri and web-origin. This is to simplify configuration for the demo. For a production system always use the real URL of the application for the redirect-uri and web-origin. 26 | 27 | ## Backend 28 | 29 | eval `minikube docker-env` 30 | docker build -t kube-demo-backend backend 31 | 32 | cat backend/backend.yaml | sed "s/KEYCLOAK_HOST/$KEYCLOAK_HOST/" | \ 33 | kubectl create -f - 34 | 35 | cat backend/backend-ingress.yaml | sed "s/BACKEND_HOST/$BACKEND_HOST/" | \ 36 | kubectl create -f - 37 | 38 | echo https://$BACKEND_HOST/public 39 | 40 | ## Frontend 41 | 42 | eval `minikube docker-env` 43 | docker build -t kube-demo-frontend frontend 44 | 45 | cat frontend/frontend.yaml | sed "s/KEYCLOAK_HOST/$KEYCLOAK_HOST/" | \ 46 | sed "s/BACKEND_HOST/$BACKEND_HOST/" | \ 47 | kubectl create -f - 48 | 49 | cat frontend/frontend-ingress.yaml | sed "s/FRONTEND_HOST/$FRONTEND_HOST/" | \ 50 | kubectl create -f - 51 | 52 | echo https://$FRONTEND_HOST 53 | 54 | The frontend application should now be opened in your browser. Login with stian/pass. You should be able to invoke public and invoke secured, but not invoke admin. To be able to invoke admin go back to the Keycloak admin console and add the `admin` role to the user `stian`. 55 | -------------------------------------------------------------------------------- /frontend/src/index.php: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | Keycloak Example App 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /backend/src/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JBoss, Home of Professional Open Source 3 | * Copyright 2016, Red Hat, Inc. and/or its affiliates, and individual 4 | * contributors by the @authors tag. See the copyright.txt in the 5 | * distribution for a full listing of individual contributors. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var express = require('express'); 19 | var session = require('express-session'); 20 | var bodyParser = require('body-parser'); 21 | var Keycloak = require('keycloak-connect'); 22 | var cors = require('cors'); 23 | 24 | var app = express(); 25 | app.use(bodyParser.json()); 26 | 27 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 28 | 29 | // Enable CORS support 30 | app.use(cors()); 31 | 32 | // Create a session-store to be used by both the express-session 33 | // middleware and the keycloak middleware. 34 | 35 | var memoryStore = new session.MemoryStore(); 36 | 37 | app.use(session({ 38 | secret: 'some secret', 39 | resave: false, 40 | saveUninitialized: true, 41 | store: memoryStore 42 | })); 43 | 44 | // Provide the session store to the Keycloak so that sessions 45 | // can be invalidated from the Keycloak console callback. 46 | // 47 | // Additional configuration is read from keycloak.json file 48 | // installed from the Keycloak web console. 49 | 50 | var keycloak = new Keycloak({ 51 | store: memoryStore 52 | }); 53 | 54 | app.use(keycloak.middleware({ 55 | logout: '/logout', 56 | admin: '/' 57 | })); 58 | 59 | app.get('/public', function (req, res) { 60 | res.json({message: 'public'}); 61 | }); 62 | 63 | app.get('/secured', keycloak.protect('realm:user'), function (req, res) { 64 | res.json({message: 'secured'}); 65 | }); 66 | 67 | app.get('/admin', keycloak.protect('realm:admin'), function (req, res) { 68 | res.json({message: 'admin'}); 69 | }); 70 | 71 | app.use('*', function (req, res) { 72 | res.send('Not found!'); 73 | }); 74 | 75 | app.listen(8080, function () { 76 | console.log('Started at port 8080'); 77 | }); 78 | -------------------------------------------------------------------------------- /frontend/src/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * JBoss, Home of Professional Open Source 3 | * Copyright 2016, Red Hat, Inc. and/or its affiliates, and individual 4 | * contributors by the @authors tag. See the copyright.txt in the 5 | * distribution for a full listing of individual contributors. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | body { 18 | background-color: #333; 19 | font-family: sans-serif; 20 | font-size: 30px; 21 | } 22 | 23 | button { 24 | font-family: sans-serif; 25 | font-size: 30px; 26 | width: 32%; 27 | 28 | background-color: #0085cf; 29 | background-image: linear-gradient(to bottom, #00a8e1 0%, #0085cf 100%); 30 | background-repeat: repeat-x; 31 | 32 | border: 2px solid #ccc; 33 | color: #fff; 34 | 35 | text-transform: uppercase; 36 | 37 | -webkit-box-shadow: 2px 2px 10px 0px rgba(0,0,0,0.5); 38 | -moz-box-shadow: 2px 2px 10px 0px rgba(0,0,0,0.5); 39 | box-shadow: 2px 2px 10px 0px rgba(0,0,0,0.5); 40 | } 41 | 42 | button:hover { 43 | background-color: #006ba6; 44 | background-image: none; 45 | -webkit-box-shadow: none; 46 | -moz-box-shadow: none; 47 | box-shadow: none; 48 | } 49 | 50 | hr { 51 | border: none; 52 | background-color: #eee; 53 | height: 10px; 54 | } 55 | 56 | .menu { 57 | padding: 10px; 58 | margin-bottom: 10px; 59 | } 60 | 61 | .content { 62 | background-color: #eee; 63 | border: 1px solid #ccc; 64 | padding: 10px; 65 | 66 | -webkit-box-shadow: 2px 2px 10px 0px rgba(0,0,0,0.5); 67 | -moz-box-shadow: 2px 2px 10px 0px rgba(0,0,0,0.5); 68 | box-shadow: 2px 2px 10px 0px rgba(0,0,0,0.5); 69 | } 70 | 71 | .content .message { 72 | padding: 20px; 73 | margin-top: 10px; 74 | background-color: #fff; 75 | border: 1px solid #ccc; 76 | font-size: 40px; 77 | font-weight: bold; 78 | } 79 | 80 | .wrapper { 81 | position: absolute; 82 | height: 230px; 83 | width: 640px; 84 | left: 50%; 85 | top: 50%; 86 | -webkit-transform: translateX(-50%) translatey(-50%); 87 | } 88 | 89 | .error { 90 | color: #a21e22; 91 | } 92 | -------------------------------------------------------------------------------- /frontend/src/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JBoss, Home of Professional Open Source 3 | * Copyright 2016, Red Hat, Inc. and/or its affiliates, and individual 4 | * contributors by the @authors tag. See the copyright.txt in the 5 | * distribution for a full listing of individual contributors. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | var keycloak = new Keycloak('keycloak.php'); 18 | 19 | function notAuthenticated() { 20 | document.getElementById('wrapper').style.display = 'block'; 21 | document.getElementById('not-authenticated').style.display = 'block'; 22 | document.getElementById('authenticated').style.display = 'none'; 23 | } 24 | 25 | function authenticated() { 26 | document.getElementById('wrapper').style.display = 'block'; 27 | document.getElementById('not-authenticated').style.display = 'none'; 28 | document.getElementById('authenticated').style.display = 'block'; 29 | document.getElementById('message').innerHTML = 'User: ' + keycloak.tokenParsed['preferred_username']; 30 | } 31 | 32 | function request(endpoint) { 33 | var req = function() { 34 | var req = new XMLHttpRequest(); 35 | var output = document.getElementById('message'); 36 | req.open('GET', serviceUrl + '/' + endpoint, true); 37 | 38 | if (keycloak.authenticated) { 39 | req.setRequestHeader('Authorization', 'Bearer ' + keycloak.token); 40 | } 41 | 42 | req.onreadystatechange = function () { 43 | if (req.readyState == 4) { 44 | if (req.status == 200) { 45 | output.innerHTML = 'Message: ' + JSON.parse(req.responseText).message; 46 | } else if (req.status == 0) { 47 | output.innerHTML = 'Request failed'; 48 | } else { 49 | output.innerHTML = '' + req.status + ' ' + req.statusText + ''; 50 | } 51 | } 52 | }; 53 | 54 | req.send(); 55 | }; 56 | 57 | if (keycloak.authenticated) { 58 | keycloak.updateToken(30).success(req); 59 | } else { 60 | req(); 61 | } 62 | } 63 | 64 | window.onload = function () { 65 | keycloak.init({ onLoad: 'check-sso', checkLoginIframeInterval: 1 }).success(function () { 66 | if (keycloak.authenticated) { 67 | authenticated(); 68 | } else { 69 | notAuthenticated(); 70 | } 71 | 72 | document.body.style.display = 'block'; 73 | }); 74 | } 75 | 76 | keycloak.onAuthLogout = notAuthenticated; 77 | --------------------------------------------------------------------------------