Skip to content

Commit a9fead4

Browse files
add saml auth option
1 parent acb6c64 commit a9fead4

File tree

31 files changed

+513
-131
lines changed

31 files changed

+513
-131
lines changed

redisinsight/api/config/default.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,16 @@ export default {
262262
redirectUri: process.env.RI_CLOUD_IDP_GH_REDIRECT_URI || process.env.RI_CLOUD_IDP_REDIRECT_URI,
263263
idp: process.env.RI_CLOUD_IDP_GH_ID,
264264
},
265+
sso: {
266+
authorizeUrl: process.env.RI_CLOUD_IDP_SSO_AUTHORIZE_URL || process.env.RI_CLOUD_IDP_AUTHORIZE_URL,
267+
tokenUrl: process.env.RI_CLOUD_IDP_SSO_TOKEN_URL || process.env.RI_CLOUD_IDP_TOKEN_URL,
268+
revokeTokenUrl: process.env.RI_CLOUD_IDP_SSO_REVOKE_TOKEN_URL || process.env.RI_CLOUD_IDP_REVOKE_TOKEN_URL,
269+
issuer: process.env.RI_CLOUD_IDP_SSO_ISSUER || process.env.RI_CLOUD_IDP_ISSUER,
270+
clientId: process.env.RI_CLOUD_IDP_SSO_CLIENT_ID || process.env.RI_CLOUD_IDP_CLIENT_ID,
271+
redirectUri: process.env.RI_CLOUD_IDP_SSO_REDIRECT_URI || process.env.RI_CLOUD_IDP_REDIRECT_URI,
272+
emailVerificationUri: process.env.RI_CLOUD_IDP_SSO_EMAIL_VERIFICATION_URI || 'saml/okta_idp_id',
273+
idp: process.env.RI_CLOUD_IDP_SSO_ID,
274+
},
265275
},
266276
},
267277
ai: {

redisinsight/api/src/constants/custom-error-codes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export enum CustomErrorCodes {
1414
CloudOauthUnexpectedError = 11_008,
1515
CloudOauthMissedRequiredData = 11_009,
1616
CloudOauthCanceled = 11_010,
17+
CloudOauthSsoUnsupportedEmail = 11_011,
1718
CloudCapiUnauthorized = 11_021,
1819
CloudCapiKeyUnauthorized = 11_022,
1920
CloudCapiKeyNotFound = 11_023,

redisinsight/api/src/constants/error-messages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export default {
8989
CLOUD_OAUTH_CANCELED: 'Authorization request was canceled.',
9090
CLOUD_OAUTH_MISCONFIGURATION: 'Authorization server misconfiguration.',
9191
CLOUD_OAUTH_GITHUB_EMAIL_PERMISSION: 'Unable to get an email from the GitHub account. Make sure that it is available.',
92+
CLOUD_OAUTH_SSO_UNSUPPORTED_EMAIL: 'Email is not recognized. Use an email associated with your organization’s SSO.',
9293
CLOUD_OAUTH_MISSED_REQUIRED_DATA: 'Unable to get required data from the user profile.',
9394
CLOUD_OAUTH_UNKNOWN_AUTHORIZATION_REQUEST: 'Unknown authorization request.',
9495
CLOUD_OAUTH_UNEXPECTED_ERROR: 'Unexpected error.',

redisinsight/api/src/modules/cloud/auth/auth-strategy/cloud-auth.strategy.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Injectable } from '@nestjs/common';
2-
import { CloudAuthRequest } from 'src/modules/cloud/auth/models/cloud-auth-request';
2+
import { CloudAuthRequest, CloudAuthRequestOptions } from 'src/modules/cloud/auth/models/cloud-auth-request';
33
import { SessionMetadata } from 'src/common/models';
44
import { OktaAuth } from '@okta/okta-auth-js';
55
import { plainToClass } from 'class-transformer';
@@ -11,7 +11,10 @@ export abstract class CloudAuthStrategy {
1111
/**
1212
* Create and store auth request params
1313
*/
14-
async generateAuthRequest(sessionMetadata: SessionMetadata): Promise<CloudAuthRequest> {
14+
async generateAuthRequest(
15+
sessionMetadata: SessionMetadata,
16+
_options?: CloudAuthRequestOptions,
17+
): Promise<CloudAuthRequest> {
1518
const authClient = new OktaAuth(this.config);
1619
const tokenParams = await authClient.token.prepareTokenParams(this.config);
1720

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { OktaAuth, SimpleStorage } from '@okta/okta-auth-js';
2+
import config from 'src/utils/config';
3+
import { CloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/cloud-auth.strategy';
4+
import {
5+
CloudAuthIdpType,
6+
CloudAuthRequest,
7+
CloudAuthRequestOptions,
8+
} from 'src/modules/cloud/auth/models/cloud-auth-request';
9+
import { SessionMetadata } from 'src/common/models';
10+
import { plainToClass } from 'class-transformer';
11+
import axios from 'axios';
12+
import * as path from 'path';
13+
import {
14+
CloudOauthSsoUnsupportedEmailException,
15+
} from 'src/modules/cloud/auth/exceptions/cloud-oauth.sso-unsupported-email.exception';
16+
import { Logger } from '@nestjs/common';
17+
18+
const { idp: { sso: idpConfig } } = config.get('cloud');
19+
const cloudConfig = config.get('cloud');
20+
21+
export class SsoIdpCloudAuthStrategy extends CloudAuthStrategy {
22+
private logger = new Logger('SsoIdpCloudAuthStrategy');
23+
24+
constructor() {
25+
super();
26+
27+
this.config = {
28+
idpType: CloudAuthIdpType.Sso,
29+
authorizeUrl: idpConfig.authorizeUrl,
30+
tokenUrl: idpConfig.tokenUrl,
31+
revokeTokenUrl: idpConfig.revokeTokenUrl,
32+
issuer: idpConfig.issuer,
33+
clientId: idpConfig.clientId,
34+
pkce: true,
35+
redirectUri: idpConfig.redirectUri,
36+
idp: idpConfig.idp,
37+
scopes: ['offline_access', 'openid', 'email', 'profile'],
38+
responseMode: 'query',
39+
responseType: 'code',
40+
tokenManager: {
41+
storage: {} as SimpleStorage,
42+
},
43+
};
44+
}
45+
46+
private async determineIdp(email: string) {
47+
try {
48+
const apiUrl = new URL(path.posix.join(cloudConfig.apiUrl, idpConfig.emailVerificationUri));
49+
apiUrl.searchParams.set('email', email);
50+
const { data: idp } = await axios.get(apiUrl.toString());
51+
52+
return idp;
53+
} catch (e) {
54+
this.logger.error('Unable to get idp by email', e);
55+
throw new CloudOauthSsoUnsupportedEmailException();
56+
}
57+
}
58+
59+
/**
60+
* Create and store auth request params
61+
*/
62+
async generateAuthRequest(
63+
sessionMetadata: SessionMetadata,
64+
options?: CloudAuthRequestOptions,
65+
): Promise<CloudAuthRequest> {
66+
const idp = await this.determineIdp(options?.data?.email);
67+
const authClient = new OktaAuth(this.config);
68+
const tokenParams = await authClient.token.prepareTokenParams(this.config);
69+
70+
return plainToClass(CloudAuthRequest, {
71+
...this.config,
72+
...tokenParams,
73+
idp,
74+
sessionMetadata,
75+
createdAt: new Date(),
76+
});
77+
}
78+
}

redisinsight/api/src/modules/cloud/auth/cloud-auth.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
22
import { CloudSessionModule } from 'src/modules/cloud/session/cloud-session.module';
33
import { GoogleIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/google-idp.cloud.auth-strategy';
44
import { GithubIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/github-idp.cloud.auth-strategy';
5+
import { SsoIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy';
56
import { CloudAuthService } from 'src/modules/cloud/auth/cloud-auth.service';
67
import { CloudAuthController } from 'src/modules/cloud/auth/cloud-auth.controller';
78
import { CloudAuthAnalytics } from 'src/modules/cloud/auth/cloud-auth.analytics';
@@ -11,6 +12,7 @@ import { CloudAuthAnalytics } from 'src/modules/cloud/auth/cloud-auth.analytics'
1112
providers: [
1213
GoogleIdpCloudAuthStrategy,
1314
GithubIdpCloudAuthStrategy,
15+
SsoIdpCloudAuthStrategy,
1416
CloudAuthService,
1517
CloudAuthAnalytics,
1618
],

redisinsight/api/src/modules/cloud/auth/cloud-auth.service.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { Injectable, Logger } from '@nestjs/common';
22
import axios from 'axios';
33
import { GoogleIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/google-idp.cloud.auth-strategy';
4-
import { CloudAuthIdpType, CloudAuthRequest } from 'src/modules/cloud/auth/models/cloud-auth-request';
4+
import {
5+
CloudAuthIdpType,
6+
CloudAuthRequest,
7+
CloudAuthRequestOptions,
8+
} from 'src/modules/cloud/auth/models/cloud-auth-request';
59
import { CloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/cloud-auth.strategy';
610
import { SessionMetadata } from 'src/common/models';
711
import { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';
812
import { GithubIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/github-idp.cloud.auth-strategy';
13+
import { SsoIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy';
914
import { wrapHttpError } from 'src/common/utils';
1015
import {
1116
CloudOauthCanceledException,
@@ -20,6 +25,9 @@ import { CloudSsoFeatureStrategy } from 'src/modules/cloud/cloud-sso.feature.fla
2025
import { EventEmitter2 } from '@nestjs/event-emitter';
2126
import { CloudAuthServerEvent } from 'src/modules/cloud/common/constants';
2227
import { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';
28+
import {
29+
CloudOauthSsoUnsupportedEmailException,
30+
} from 'src/modules/cloud/auth/exceptions/cloud-oauth.sso-unsupported-email.exception';
2331

2432
@Injectable()
2533
export class CloudAuthService {
@@ -31,10 +39,19 @@ export class CloudAuthService {
3139
private readonly sessionService: CloudSessionService,
3240
private readonly googleIdpAuthStrategy: GoogleIdpCloudAuthStrategy,
3341
private readonly githubIdpCloudAuthStrategy: GithubIdpCloudAuthStrategy,
42+
private readonly ssoIdpCloudAuthStrategy: SsoIdpCloudAuthStrategy,
3443
private readonly analytics: CloudAuthAnalytics,
3544
private readonly eventEmitter: EventEmitter2,
3645
) {}
3746

47+
static getOAuthHttpRequestHeaders() {
48+
return {
49+
accept: 'application/json',
50+
'cache-control': 'no-cache',
51+
'content-type': 'application/x-www-form-urlencoded',
52+
};
53+
}
54+
3855
static getAuthorizationServerRedirectError(
3956
query: { error_description: string, error: string },
4057
authRequest?: CloudAuthRequest,
@@ -69,6 +86,8 @@ export class CloudAuthService {
6986
return this.googleIdpAuthStrategy;
7087
case CloudAuthIdpType.GitHub:
7188
return this.githubIdpCloudAuthStrategy;
89+
case CloudAuthIdpType.Sso:
90+
return this.ssoIdpCloudAuthStrategy;
7291
default:
7392
throw new CloudOauthUnknownAuthorizationRequestException('Unknown cloud auth strategy');
7493
}
@@ -81,14 +100,11 @@ export class CloudAuthService {
81100
*/
82101
async getAuthorizationUrl(
83102
sessionMetadata: SessionMetadata,
84-
options: {
85-
strategy: CloudAuthIdpType,
86-
action?: string,
87-
callback?: Function,
88-
},
103+
options: CloudAuthRequestOptions,
89104
): Promise<string> {
90105
try {
91-
const authRequest: any = await this.getAuthStrategy(options?.strategy).generateAuthRequest(sessionMetadata);
106+
const authRequest: any = await this.getAuthStrategy(options?.strategy)
107+
.generateAuthRequest(sessionMetadata, options);
92108
authRequest.callback = options?.callback;
93109
authRequest.action = options?.action;
94110

@@ -100,6 +116,10 @@ export class CloudAuthService {
100116

101117
return CloudAuthStrategy.generateAuthUrl(authRequest).toString();
102118
} catch (e) {
119+
if (e instanceof CloudOauthSsoUnsupportedEmailException) {
120+
throw e;
121+
}
122+
103123
throw new CloudOauthMisconfigurationException();
104124
}
105125
}
@@ -114,11 +134,7 @@ export class CloudAuthService {
114134
const tokenUrl = CloudAuthStrategy.generateExchangeCodeUrl(authRequest, code);
115135

116136
const { data } = await axios.post(tokenUrl.toString().split('?')[0], tokenUrl.searchParams, {
117-
headers: {
118-
accept: 'application/json',
119-
'cache-control': 'no-cache',
120-
'content-type': 'application/x-www-form-urlencoded',
121-
},
137+
headers: CloudAuthService.getOAuthHttpRequestHeaders(),
122138
});
123139

124140
return data;
@@ -195,11 +211,7 @@ export class CloudAuthService {
195211

196212
await axios.post(tokenUrl.toString()
197213
.split('?')[0], tokenUrl.searchParams, {
198-
headers: {
199-
accept: 'application/json',
200-
'cache-control': 'no-cache',
201-
'content-type': 'application/x-www-form-urlencoded',
202-
},
214+
headers: CloudAuthService.getOAuthHttpRequestHeaders(),
203215
});
204216
} catch (e) {
205217
// ignore error
@@ -252,11 +264,7 @@ export class CloudAuthService {
252264

253265
const { data } = await axios.post(tokenUrl.toString()
254266
.split('?')[0], tokenUrl.searchParams, {
255-
headers: {
256-
accept: 'application/json',
257-
'cache-control': 'no-cache',
258-
'content-type': 'application/x-www-form-urlencoded',
259-
},
267+
headers: CloudAuthService.getOAuthHttpRequestHeaders(),
260268
});
261269

262270
await this.sessionService.updateSessionData(sessionMetadata.sessionId, {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common';
2+
import { CustomErrorCodes } from 'src/constants';
3+
import ERROR_MESSAGES from 'src/constants/error-messages';
4+
5+
export class CloudOauthSsoUnsupportedEmailException extends HttpException {
6+
constructor(message = ERROR_MESSAGES.CLOUD_OAUTH_SSO_UNSUPPORTED_EMAIL, options?: HttpExceptionOptions) {
7+
const response = {
8+
message,
9+
statusCode: HttpStatus.BAD_REQUEST,
10+
error: 'CloudOauthSsoUnsupportedEmail',
11+
errorCode: CustomErrorCodes.CloudOauthSsoUnsupportedEmail,
12+
};
13+
14+
super(response, response.statusCode, options);
15+
}
16+
}

redisinsight/api/src/modules/cloud/auth/models/cloud-auth-request.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,23 @@ import { SessionMetadata } from 'src/common/models';
33
export enum CloudAuthIdpType {
44
Google = 'google',
55
GitHub = 'github',
6+
Sso = 'sso',
67
}
78

8-
export class CloudAuthRequest {
9-
idpType: CloudAuthIdpType;
9+
export class CloudAuthRequestOptions {
10+
strategy: CloudAuthIdpType;
1011

11-
sessionMetadata: SessionMetadata;
12+
action?: string;
13+
14+
data?: Record<string, any>;
1215

1316
callback?: Function;
17+
}
1418

15-
createdAt: Date;
19+
export class CloudAuthRequest extends CloudAuthRequestOptions {
20+
idpType: CloudAuthIdpType;
1621

17-
action?: string;
22+
sessionMetadata: SessionMetadata;
23+
24+
createdAt: Date;
1825
}

redisinsight/desktop/src/lib/cloud/cloud-oauth.handlers.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { UrlWithParsedQuery } from 'url'
55
import { wrapErrorMessageSensitiveData } from 'desktopSrc/utils'
66
import { getBackendApp, getWindows } from 'desktopSrc/lib'
77
import { IpcOnEvent, IpcInvokeEvent } from 'uiSrc/electron/constants'
8-
import { CloudAuthIdpType, CloudAuthResponse, CloudAuthStatus } from 'apiSrc/modules/cloud/auth/models'
8+
import {
9+
CloudAuthRequestOptions,
10+
CloudAuthResponse,
11+
CloudAuthStatus,
12+
} from 'apiSrc/modules/cloud/auth/models'
913
import { DEFAULT_SESSION_ID, DEFAULT_USER_ID } from 'apiSrc/common/constants'
1014
import { CloudOauthUnexpectedErrorException } from 'apiSrc/modules/cloud/auth/exceptions'
1115
import { CloudAuthService } from '../../../../api/dist/src/modules/cloud/auth/cloud-auth.service'
@@ -29,11 +33,9 @@ export const getTokenCallbackFunction = (webContents: WebContents) => (response:
2933
webContents.send(IpcOnEvent.cloudOauthCallback, response)
3034
webContents.focus()
3135
}
36+
3237
export const initCloudOauthHandlers = () => {
33-
ipcMain.handle(IpcInvokeEvent.cloudOauth, async (event, options: {
34-
strategy: CloudAuthIdpType,
35-
action?: string,
36-
}) => {
38+
ipcMain.handle(IpcInvokeEvent.cloudOauth, async (event, options: CloudAuthRequestOptions) => {
3739
try {
3840
const authService: CloudAuthService = getBackendApp()?.get?.(CloudAuthService)
3941

@@ -57,9 +59,16 @@ export const initCloudOauthHandlers = () => {
5759
} catch (e) {
5860
log.error(wrapErrorMessageSensitiveData(e as Error))
5961

62+
const error = getOauthIpcErrorResponse(e)
63+
6064
const [currentWindow] = getWindows().values()
6165

62-
currentWindow?.webContents.send(IpcOnEvent.cloudOauthCallback, getOauthIpcErrorResponse(e))
66+
currentWindow?.webContents.send(IpcOnEvent.cloudOauthCallback, error)
67+
68+
return {
69+
status: CloudAuthStatus.Failed,
70+
error,
71+
}
6372
}
6473
})
6574
}

0 commit comments

Comments
 (0)