Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Commit d19d22e

Browse files
Allow using keycloak tokens
1 parent d0964f2 commit d19d22e

10 files changed

+208
-41
lines changed

Diff for: .env.example

+5
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ GITHUB_API_URL=https://api.github.com
1414
GITHUB_CLIENT_ID=redacted
1515
GITHUB_CLIENT_SECRET=redacted
1616
GITHUB_OAUTH_TOKEN=redacted
17+
KEYCLOAK_API_URL=http://localhost:8080/auth
18+
KEYCLOAK_CLIENT_ID=redacted
19+
KEYCLOAK_CLIENT_SECRET=redacted
20+
KEYCLOAK_REALM=redacted
1721
HASH_SECRET=redacted
1822
COTURN_USERNAME=redacted
1923
COTURN_PASSWORD=redacted
2024
ACTIVE_PUB_SUB_GATEWAY=pusher
2125
ACTIVE_ICE_SERVER_PROVIDER=twilio
26+
ACTIVE_IDENTITY_PROVIDER=github

Diff for: .env.local.example

+5
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ GITHUB_API_URL=https://api.github.com
1414
GITHUB_CLIENT_ID=redacted
1515
GITHUB_CLIENT_SECRET=redacted
1616
GITHUB_OAUTH_TOKEN=redacted
17+
KEYCLOAK_API_URL=http://localhost:8080/auth
18+
KEYCLOAK_CLIENT_ID=teletype
19+
KEYCLOAK_CLIENT_SECRET=redacted
20+
KEYCLOAK_REALM=teletype
1721
HASH_SECRET=redacted
1822
COTURN_USERNAME=teletype
1923
COTURN_PASSWORD=password
2024
ACTIVE_PUB_SUB_GATEWAY=socketcluster
2125
ACTIVE_ICE_SERVER_PROVIDER=coturn
26+
ACTIVE_IDENTITY_PROVIDER=keycloak

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ node_modules
33
newrelic_agent.log
44
database
55
test-database
6+
keycloak-database

Diff for: app.json

+16
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,30 @@
4444
"COTURN_USERNAME": {
4545
"required": true
4646
},
47+
4748
"COTURN_PASSWORD": {
4849
"required": true
4950
},
51+
"KEYCLOAK_API_URL": {
52+
"required": true
53+
},
54+
"KEYCLOAK_CLIENT_ID": {
55+
"required": true
56+
},
57+
"KEYCLOAK_CLIENT_SECRET": {
58+
"required": true
59+
},
60+
"KEYCLOAK_REALM": {
61+
"required": true
62+
},
5063
"ACTIVE_PUB_SUB_GATEWAY": {
5164
"required": true
5265
},
5366
"ACTIVE_ICE_SERVER_PROVIDER": {
5467
"required": true
68+
},
69+
"ACTIVE_IDENTITY_PROVIDER": {
70+
"required": true
5571
}
5672
},
5773
"formation": {},

Diff for: docker-compose.yml

+32
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,37 @@ services:
5555
aliases:
5656
- socketcluster
5757

58+
keycloak:
59+
image: jboss/keycloak
60+
ports:
61+
- 8080:8080
62+
restart: always
63+
environment:
64+
DB_VENDOR: postgres
65+
DB_ADDR: keycloak-database
66+
KEYCLOAK_USER: teletype
67+
KEYCLOAK_PASSWORD: password
68+
networks:
69+
teletype:
70+
aliases:
71+
- keycloak
72+
73+
keycloak-database:
74+
image: postgres
75+
ports:
76+
- 5434:5432
77+
volumes:
78+
- ./keycloak-database:/var/lib/postgresql/data
79+
restart: always
80+
environment:
81+
POSTGRES_USER: keycloak
82+
POSTGRES_PASSWORD: password
83+
POSTGRES_DB: keycloak
84+
networks:
85+
teletype:
86+
aliases:
87+
- keycloak-database
88+
89+
5890
networks:
5991
teletype:

Diff for: index.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,18 @@ async function startServer (id) {
2323
githubClientId: process.env.GITHUB_CLIENT_ID,
2424
githubClientSecret: process.env.GITHUB_CLIENT_SECRET,
2525
githubOauthToken: process.env.GITHUB_OAUTH_TOKEN,
26+
keycloakApiUrl: process.env.KEYCLOAK_API_URL,
27+
keycloakClientId: process.env.KEYCLOAK_CLIENT_ID,
28+
keycloakClientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
29+
keycloakRealm: process.env.KEYCLOAK_REALM,
2630
boomtownSecret: process.env.BOOMTOWN_SECRET,
2731
hashSecret: process.env.HASH_SECRET,
2832
port: process.env.PORT || 3000,
2933
coturnUsername: process.env.COTURN_USERNAME,
3034
coturnPassword: process.env.COTURN_PASSWORD,
3135
activePubSubGateway: process.env.ACTIVE_PUB_SUB_GATEWAY,
32-
activeIceServerProvider: process.env.ACTIVE_ICE_SERVER_PROVIDER
36+
activeIceServerProvider: process.env.ACTIVE_ICE_SERVER_PROVIDER,
37+
activeIdentityProvider: process.env.ACTIVE_IDENTITY_PROVIDER
3338
})
3439
await server.start()
3540
console.log(`Worker ${id} (pid: ${process.pid}): listening on port ${server.port}`)

Diff for: lib/identity-provider.js renamed to lib/github-identity-provider.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const {StatusCodeError} = require('request-promise-core/lib/errors')
22

33
module.exports =
4-
class IdentityProvider {
4+
class GithubIdentityProvider {
55
constructor ({request, apiUrl, clientId, clientSecret, oauthToken}) {
66
this.request = request
77
this.apiUrl = apiUrl

Diff for: lib/keycloak-identity-provider.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const {StatusCodeError} = require('request-promise-core/lib/errors')
2+
3+
module.exports =
4+
class KeycloakIdentityProvider {
5+
constructor ({request, apiUrl, clientId, clientSecret, realm}) {
6+
this.request = request
7+
this.apiUrl = apiUrl
8+
this.clientId = clientId
9+
this.clientSecret = clientSecret
10+
this.realm = realm
11+
}
12+
13+
make_base_auth(clientId, clientSecret) {
14+
var token = clientId + ':' + clientSecret
15+
var hash = Buffer.from(token).toString('base64')
16+
return "Basic " + hash
17+
}
18+
19+
async identityForToken (oauthToken) {
20+
try {
21+
var options = {
22+
method: 'POST',
23+
uri: `${this.apiUrl}/realms/${this.realm}/protocol/openid-connect/token/introspect`,
24+
headers: {
25+
'Content-Type': 'application/x-www-form-urlencoded',
26+
'User-Agent': 'api.teletype.atom.io',
27+
'Authorization': this.make_base_auth(this.clientId, this.clientSecret)
28+
},
29+
body: `token=${oauthToken}`
30+
}
31+
const response = await this.request(options)
32+
const user = JSON.parse(response)
33+
34+
if (!user.active) {
35+
const error = new Error('Token not provided.')
36+
error.statusCode = 400
37+
throw error
38+
}
39+
return {id: user.sub, login: user.username}
40+
} catch (e) {
41+
let errorMessage, statusCode
42+
if (e instanceof StatusCodeError) {
43+
const error = JSON.parse(e.error)
44+
const description = (error.message != null) ? error.message : e.error
45+
errorMessage = `${this.apiUrl} responded with ${e.statusCode}: ${description}`
46+
statusCode = e.statusCode
47+
} else if (e instanceof Error) {
48+
errorMessage = `Failed to query ${this.apiUrl}: ${e.message}`
49+
statusCode = 400
50+
} else {
51+
errorMessage = `Failed to query ${this.apiUrl}: ${e.message}`
52+
statusCode = 500
53+
}
54+
55+
const error = new Error(errorMessage)
56+
error.statusCode = statusCode
57+
throw error
58+
}
59+
}
60+
61+
async isOperational () {
62+
try {
63+
var options = {
64+
method: 'POST',
65+
uri: `${this.apiUrl}/realms/${this.realm}/protocol/openid-connect/token/introspect`,
66+
headers: {
67+
'Content-Type': 'application/x-www-form-urlencoded',
68+
'User-Agent': 'api.teletype.atom.io',
69+
'Authorization': this.make_base_auth(this.clientId, this.clientSecret)
70+
},
71+
body: `token=`
72+
}
73+
74+
await this.request(options)
75+
return true
76+
} catch (e) {
77+
return false
78+
}
79+
}
80+
}

Diff for: lib/server.js

+56-33
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
const buildControllerLayer = require('./controller-layer')
22
const pgp = require('pg-promise')()
33
const request = require('request-promise-native')
4-
const IdentityProvider = require('./identity-provider')
4+
const GithubIdentityProvider = require('./github-identity-provider')
5+
const KeycloakIdentityProvider = require('./keycloak-identity-provider')
56
const ModelLayer = require('./model-layer')
67
const PusherPubSubGateway = require('./pusher-pub-sub-gateway')
78
const SocketClusterPubSubGateway = require('./socketcluster-pub-sub-gateway')
@@ -22,59 +23,81 @@ class Server {
2223
this.githubClientId = options.githubClientId
2324
this.githubClientSecret = options.githubClientSecret
2425
this.githubOauthToken = options.githubOauthToken
26+
this.keycloakApiUrl = options.keycloakApiUrl
27+
this.keycloakClientId = options.keycloakClientId
28+
this.keycloakClientSecret = options.keycloakClientSecret
29+
this.keycloakRealm = options.keycloakRealm
2530
this.boomtownSecret = options.boomtownSecret
2631
this.hashSecret = options.hashSecret
2732
this.port = options.port,
2833
this.coturnUsername = options.coturnUsername,
2934
this.coturnPassword = options.coturnPassword,
3035
this.activePubSubGateway = options.activePubSubGateway,
3136
this.activeIceServerProvider = options.activeIceServerProvider
37+
this.activeIdentityProvider = options.activeIdentityProvider
3238
}
3339

3440
async start () {
3541
const modelLayer = new ModelLayer({db: pgp(this.databaseURL), hashSecret: this.hashSecret})
36-
const identityProvider = new IdentityProvider({
37-
request,
38-
apiUrl: this.githubApiUrl,
39-
clientId: this.githubClientId,
40-
clientSecret: this.githubClientSecret,
41-
oauthToken: this.githubOauthToken
42-
})
42+
43+
var identityProvider
44+
45+
switch(this.activeIdentityProvider) {
46+
case 'keycloak':
47+
identityProvider = new KeycloakIdentityProvider({
48+
request,
49+
apiUrl: this.keycloakApiUrl,
50+
clientId: this.keycloakClientId,
51+
clientSecret: this.keycloakClientSecret,
52+
realm: this.keycloakRealm
53+
})
54+
break;
55+
case 'github':
56+
default:
57+
identityProvider = new GithubIdentityProvider({
58+
request,
59+
apiUrl: this.githubApiUrl,
60+
clientId: this.githubClientId,
61+
clientSecret: this.githubClientSecret,
62+
oauthToken: this.githubOauthToken
63+
})
64+
break;
65+
}
4366

4467
var pubSubGateway
4568

4669
switch(this.activePubSubGateway) {
47-
case 'socketcluster':
48-
pubSubGateway = new SocketClusterPubSubGateway({})
49-
break;
70+
case 'socketcluster':
71+
pubSubGateway = new SocketClusterPubSubGateway({})
72+
break;
5073

51-
case 'pusher':
52-
default:
53-
pubSubGateway = new PusherPubSubGateway({
54-
appId: this.pusherAppId,
55-
key: this.pusherKey,
56-
secret: this.pusherSecret,
57-
cluster: this.pusherCluster
58-
})
59-
break;
74+
case 'pusher':
75+
default:
76+
pubSubGateway = new PusherPubSubGateway({
77+
appId: this.pusherAppId,
78+
key: this.pusherKey,
79+
secret: this.pusherSecret,
80+
cluster: this.pusherCluster
81+
})
82+
break;
6083
}
6184

6285
var iceServerProvider
6386
switch(this.activeIceServerProvider) {
64-
case 'coturn':
65-
iceServerProvider = new CoturnIceServerProvider({
66-
coturnUsername: this.coturnUsername,
67-
coturnPassword: this.coturnPassword
68-
})
69-
break;
87+
case 'coturn':
88+
iceServerProvider = new CoturnIceServerProvider({
89+
coturnUsername: this.coturnUsername,
90+
coturnPassword: this.coturnPassword
91+
})
92+
break;
7093

71-
case 'twilio':
72-
default:
73-
iceServerProvider = new TwilioIceServerProvider({
74-
twilioAccount: this.twilioAccount,
75-
twilioAuthToken: this.twilioAuthToken
76-
})
77-
break;
94+
case 'twilio':
95+
default:
96+
iceServerProvider = new TwilioIceServerProvider({
97+
twilioAccount: this.twilioAccount,
98+
twilioAuthToken: this.twilioAuthToken
99+
})
100+
break;
78101
}
79102

80103
const controllerLayer = buildControllerLayer({

Diff for: test/identity-provider.test.js renamed to test/github-identity-provider.test.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
const assert = require('assert')
22
const {RequestError, StatusCodeError} = require('request-promise-core/lib/errors')
3-
const IdentityProvider = require('../lib/identity-provider')
3+
const GithubIdentityProvider = require('../lib/github-identity-provider')
44

5-
suite('IdentityProvider', () => {
5+
suite('GithubIdentityProvider', () => {
66
test('returns user associated with OAuth token', async () => {
77
const request = {
88
get: async function (url, {headers}) {
@@ -19,7 +19,7 @@ suite('IdentityProvider', () => {
1919
}
2020
}
2121

22-
const provider = new IdentityProvider({request})
22+
const provider = new GithubIdentityProvider({request})
2323

2424
const user1 = await provider.identityForToken('user-1-token')
2525
assert.deepEqual(user1, {id: '1', login: 'user-1'})
@@ -36,7 +36,7 @@ suite('IdentityProvider', () => {
3636
}
3737
}
3838

39-
const provider = new IdentityProvider({request})
39+
const provider = new GithubIdentityProvider({request})
4040

4141
let error = null
4242
try {
@@ -56,7 +56,7 @@ suite('IdentityProvider', () => {
5656
}
5757
}
5858

59-
const provider = new IdentityProvider({request})
59+
const provider = new GithubIdentityProvider({request})
6060

6161
let error = null
6262
try {
@@ -75,7 +75,7 @@ suite('IdentityProvider', () => {
7575
}
7676
}
7777

78-
const provider = new IdentityProvider({request})
78+
const provider = new GithubIdentityProvider({request})
7979

8080
let error = null
8181
try {

0 commit comments

Comments
 (0)