1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const tslib_1 = require("tslib");
|
4 | const config_1 = tslib_1.__importDefault(require("keycloak-connect/middleware/auth-utils/config"));
|
5 | const grant_manager_1 = tslib_1.__importDefault(require("keycloak-connect/middleware/auth-utils/grant-manager"));
|
6 | const hapi_1 = require("@hapi/hapi");
|
7 | const v1_1 = tslib_1.__importDefault(require("uuid/v1"));
|
8 | const querystring_1 = tslib_1.__importDefault(require("querystring"));
|
9 | const path_1 = tslib_1.__importDefault(require("path"));
|
10 | const inert_1 = tslib_1.__importDefault(require("@hapi/inert"));
|
11 | const open_1 = tslib_1.__importDefault(require("open"));
|
12 | const debug_1 = tslib_1.__importDefault(require("debug"));
|
13 | const utils_1 = require("../utils");
|
14 | const sdk_1 = require("@cto.ai/sdk");
|
15 | const axios_1 = tslib_1.__importDefault(require("axios"));
|
16 | const CustomErrors_1 = require("../errors/CustomErrors");
|
17 | const env_1 = require("../constants/env");
|
18 | const jsonwebtoken_1 = tslib_1.__importDefault(require("jsonwebtoken"));
|
19 | const debug = debug_1.default('ops:KeycloakService');
|
20 | const KEYCLOAK_CONFIG = {
|
21 | realm: 'ops',
|
22 | 'auth-server-url': env_1.OPS_KEYCLOAK_HOST,
|
23 | 'ssl-required': 'external',
|
24 | resource: 'ops-cli',
|
25 | 'public-client': true,
|
26 | 'confidential-port': 0,
|
27 | };
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | const keycloakTokenEndpoint = `${KEYCLOAK_CONFIG['auth-server-url']}/realms/${KEYCLOAK_CONFIG.realm}/protocol/openid-connect/token`;
|
36 | const accountUrl = `${KEYCLOAK_CONFIG['auth-server-url']}/realms/${KEYCLOAK_CONFIG.realm}/account/password`;
|
37 | class KeycloakService {
|
38 | constructor(grantManager = new grant_manager_1.default(new config_1.default(KEYCLOAK_CONFIG))) {
|
39 | this.grantManager = grantManager;
|
40 | this.KEYCLOAK_SIGNIN_FILEPATH = path_1.default.join(__dirname, '../keycloakPages/signinRedirect.html');
|
41 | this.KEYCLOAK_SIGNUP_FILEPATH = path_1.default.join(__dirname, '../keycloakPages/signupRedirect.html');
|
42 | this.KEYCLOAK_ERROR_FILEPATH = path_1.default.join(__dirname, '../keycloakPages/errorRedirect.html');
|
43 | this.KEYCLOAK_REALM = 'ops';
|
44 | this.CALLBACK_HOST = 'localhost';
|
45 | this.CALLBACK_ENDPOINT = 'callback';
|
46 | this.CLIENT_ID = 'ops-cli';
|
47 | this.CONFIDENTIAL_CLIENT_ID = 'ops-cli-confidential';
|
48 | this.CALLBACK_PORT = null;
|
49 | this.CALLBACK_URL = null;
|
50 | this.POSSIBLE_PORTS = [10234, 28751, 38179, 41976, 49164];
|
51 | this.hapiServer = {};
|
52 | |
53 |
|
54 |
|
55 | this._buildStandardFlowParams = () => {
|
56 | const data = {
|
57 | client_id: this.CLIENT_ID,
|
58 | redirect_uri: this.CALLBACK_URL,
|
59 | response_type: 'code',
|
60 | scope: 'openid token',
|
61 | nonce: v1_1.default(),
|
62 | state: v1_1.default(),
|
63 | };
|
64 | return querystring_1.default.stringify(data);
|
65 | };
|
66 | |
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 | this._buildAuthorizeUrl = () => {
|
78 | const params = this._buildStandardFlowParams();
|
79 | return `${KEYCLOAK_CONFIG['auth-server-url']}/realms/${this.KEYCLOAK_REALM}/protocol/openid-connect/auth?${params}`;
|
80 | };
|
81 | |
82 |
|
83 |
|
84 | this._formatGrantToTokens = (grant) => {
|
85 | if (!grant ||
|
86 | !grant.access_token ||
|
87 | !grant.refresh_token ||
|
88 | !grant.id_token ||
|
89 | !grant.access_token.content ||
|
90 | !grant.access_token.content.session_state) {
|
91 | throw new CustomErrors_1.SSOError();
|
92 | }
|
93 | const accessToken = grant.access_token.token;
|
94 | const refreshToken = grant.refresh_token.token;
|
95 | const idToken = grant.id_token.token;
|
96 | const sessionState = grant.access_token.content.session_state;
|
97 | if (!accessToken || !refreshToken || !idToken)
|
98 | throw new CustomErrors_1.SSOError();
|
99 | return {
|
100 | accessToken,
|
101 | refreshToken,
|
102 | idToken,
|
103 | sessionState,
|
104 | };
|
105 | };
|
106 | |
107 |
|
108 |
|
109 | this.keycloakSignInFlow = async () => {
|
110 | open_1.default(this._buildAuthorizeUrl());
|
111 | const grant = await this._setupCallbackServerForGrant('signin');
|
112 | return this._formatGrantToTokens(grant);
|
113 | };
|
114 | |
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 | this._buildRegisterUrl = () => {
|
122 | const params = this._buildStandardFlowParams();
|
123 | return `${KEYCLOAK_CONFIG['auth-server-url']}/realms/${this.KEYCLOAK_REALM}/protocol/openid-connect/registrations?${params}`;
|
124 | };
|
125 | |
126 |
|
127 |
|
128 | this.keycloakSignUpFlow = async () => {
|
129 | const registerUrl = this._buildRegisterUrl();
|
130 | open_1.default(registerUrl);
|
131 | console.log(`\n💻 Please follow the prompts in the browser window and verify your email address before logging in`);
|
132 | console.log(`\n If the link doesn't open, please click the following URL ${sdk_1.ux.colors.dim(registerUrl)} \n\n`);
|
133 | const grant = await this._setupCallbackServerForGrant('signup');
|
134 | return this._formatGrantToTokens(grant);
|
135 | };
|
136 | |
137 |
|
138 |
|
139 |
|
140 |
|
141 | this._buildResetUrl = () => {
|
142 | const data = {
|
143 | client_id: this.CLIENT_ID,
|
144 | };
|
145 | const params = querystring_1.default.stringify(data);
|
146 | return `${KEYCLOAK_CONFIG['auth-server-url']}/realms/${this.KEYCLOAK_REALM}/login-actions/reset-credentials?${params}`;
|
147 | };
|
148 | this.keycloakResetFlow = (isUserSignedIn) => {
|
149 |
|
150 | const url = isUserSignedIn ? accountUrl : this._buildResetUrl();
|
151 | open_1.default(url);
|
152 | console.log(`\n💻 Please follow the prompts in the browser window`);
|
153 | console.log(`\n If the link doesn't open, please click the following URL: ${sdk_1.ux.colors.dim(url)} \n\n`);
|
154 | };
|
155 | |
156 |
|
157 |
|
158 |
|
159 | this.includeClientSecret = (clientName) => {
|
160 | return clientName === this.CLIENT_ID
|
161 | ? {}
|
162 | : {
|
163 | client_secret: env_1.OPS_CLIENT_SECRET,
|
164 | };
|
165 | };
|
166 | this.refreshAccessToken = async (oldConfig, refreshToken) => {
|
167 | try {
|
168 | debug('Starting to refresh access token');
|
169 | const decodedToken = jsonwebtoken_1.default.decode(refreshToken);
|
170 | |
171 |
|
172 |
|
173 |
|
174 |
|
175 | const clientName = decodedToken && decodedToken.azp ? decodedToken.azp : this.CLIENT_ID;
|
176 | |
177 |
|
178 |
|
179 | const refreshData = querystring_1.default.stringify(Object.assign({ grant_type: 'refresh_token', client_id: clientName, refresh_token: refreshToken }, this.includeClientSecret(clientName)));
|
180 | const { data, } = await axios_1.default.post(keycloakTokenEndpoint, refreshData);
|
181 | if (!data.access_token || !data.refresh_token || !data.id_token)
|
182 | throw new CustomErrors_1.SSOError('There are UAA tokens missing.');
|
183 | debug('Successfully refreshed access token');
|
184 | return {
|
185 | accessToken: data.access_token,
|
186 | refreshToken: data.refresh_token,
|
187 | idToken: data.id_token,
|
188 | sessionState: oldConfig.tokens.sessionState,
|
189 | };
|
190 | }
|
191 | catch (error) {
|
192 | debug('%O', error);
|
193 |
|
194 | throw new CustomErrors_1.SSOError();
|
195 | }
|
196 | };
|
197 | this.getTokenFromPasswordGrant = async ({ user, password, }) => {
|
198 | try {
|
199 | |
200 |
|
201 |
|
202 | debug('getting token from password grant');
|
203 | const postBody = querystring_1.default.stringify({
|
204 | grant_type: 'password',
|
205 | client_id: this.CONFIDENTIAL_CLIENT_ID,
|
206 | client_secret: env_1.OPS_CLIENT_SECRET,
|
207 | username: user,
|
208 | password,
|
209 | scope: 'openid',
|
210 | });
|
211 | const { data, } = await axios_1.default.post(keycloakTokenEndpoint, postBody);
|
212 | if (!data.access_token ||
|
213 | !data.refresh_token ||
|
214 | !data.id_token ||
|
215 | !data.session_state)
|
216 | throw new CustomErrors_1.SSOError('There are UAA tokens missing.');
|
217 | return {
|
218 | accessToken: data.access_token,
|
219 | refreshToken: data.refresh_token,
|
220 | idToken: data.id_token,
|
221 | sessionState: data.session_state,
|
222 | };
|
223 | }
|
224 | catch (error) {
|
225 | debug('%O', error);
|
226 | throw new CustomErrors_1.SSOError();
|
227 | }
|
228 | };
|
229 | |
230 |
|
231 |
|
232 |
|
233 | this._setupCallbackServerForGrant = async (caller) => {
|
234 | let redirectFilePath = caller === 'signin'
|
235 | ? this.KEYCLOAK_SIGNIN_FILEPATH
|
236 | : this.KEYCLOAK_SIGNUP_FILEPATH;
|
237 | return new Promise(async (resolve, reject) => {
|
238 | try {
|
239 | let responsePayload;
|
240 | await this.hapiServer.register(inert_1.default);
|
241 | this.hapiServer.route({
|
242 | method: 'GET',
|
243 | path: `/${this.CALLBACK_ENDPOINT}`,
|
244 | handler: async (req, reply) => {
|
245 | try {
|
246 | if (req.query.code) {
|
247 | await this.grantManager
|
248 | .obtainFromCode(
|
249 | |
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 | {
|
261 | session: {
|
262 | auth_redirect_uri: this.CALLBACK_URL,
|
263 | },
|
264 | }, req.query.code)
|
265 | .then((res) => {
|
266 | responsePayload = res;
|
267 | });
|
268 | }
|
269 | else {
|
270 | redirectFilePath = this.KEYCLOAK_ERROR_FILEPATH;
|
271 | }
|
272 |
|
273 | return reply.file(redirectFilePath, {
|
274 | confine: false,
|
275 | });
|
276 | }
|
277 | catch (err) {
|
278 | debug('%O', err);
|
279 | reject(err);
|
280 | }
|
281 | finally {
|
282 | if (responsePayload) {
|
283 | this.hapiServer.stop();
|
284 | resolve(responsePayload);
|
285 | }
|
286 | else {
|
287 | sdk_1.ux.spinner.stop('failed');
|
288 | this.hapiServer.stop();
|
289 | }
|
290 | }
|
291 | },
|
292 | });
|
293 | |
294 |
|
295 |
|
296 | await this.hapiServer.start();
|
297 | }
|
298 | catch (err) {
|
299 | debug('%O', err);
|
300 | reject(err);
|
301 | }
|
302 | });
|
303 | };
|
304 | |
305 |
|
306 |
|
307 | this.buildInvalidateSessionUrl = () => {
|
308 | return `${env_1.OPS_KEYCLOAK_HOST}/realms/ops/protocol/openid-connect/logout`;
|
309 | };
|
310 | |
311 |
|
312 |
|
313 | this.buildInvalidateSessionHeaders = (sessionState, accessToken) => {
|
314 | return {
|
315 | Cookie: `$KEYCLOAK_SESSION=ops/${sessionState}; KEYCLOAK_IDENTITY=${accessToken}`,
|
316 | };
|
317 | };
|
318 | }
|
319 |
|
320 | async init() {
|
321 | const CALLBACK_PORT = await utils_1.getFirstActivePort(this.POSSIBLE_PORTS);
|
322 | if (!CALLBACK_PORT)
|
323 | throw new Error('Cannot find available port');
|
324 | this.CALLBACK_PORT = CALLBACK_PORT;
|
325 | this.hapiServer = new hapi_1.Server({
|
326 | port: CALLBACK_PORT,
|
327 | host: this.CALLBACK_HOST,
|
328 | });
|
329 | this.CALLBACK_URL = `http://${this.CALLBACK_HOST}:${CALLBACK_PORT}/${this.CALLBACK_ENDPOINT}`;
|
330 | }
|
331 | }
|
332 | exports.KeycloakService = KeycloakService;
|