diff --git a/services/authentication-service/.env.example b/services/authentication-service/.env.example
index b6b767561c..3d351f2665 100644
--- a/services/authentication-service/.env.example
+++ b/services/authentication-service/.env.example
@@ -82,4 +82,4 @@ AZURE_AUTH_COOKIE_IV=
AUTH0_DOMAIN=
AUTH0_CLIENT_ID=
AUTH0_CLIENT_SECRET=
-AUTH0_CALLBACK_URL=
\ No newline at end of file
+AUTH0_CALLBACK_URL=
diff --git a/services/authentication-service/.vscode/settings.json b/services/authentication-service/.vscode/settings.json
index 07313667ec..4d57e61980 100644
--- a/services/authentication-service/.vscode/settings.json
+++ b/services/authentication-service/.vscode/settings.json
@@ -5,8 +5,8 @@
"editor.trimAutoWhitespace": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
- "source.organizeImports": true,
- "source.fixAll.eslint": true
+ "source.organizeImports": "explicit",
+ "source.fixAll.eslint": "explicit"
},
"files.exclude": {
diff --git a/services/authentication-service/openapi.json b/services/authentication-service/openapi.json
index 0e3ed478a6..563a06a49d 100644
--- a/services/authentication-service/openapi.json
+++ b/services/authentication-service/openapi.json
@@ -7,6 +7,40 @@
"contact": {}
},
"paths": {
+ "/.well-known/openid-configuration": {
+ "get": {
+ "x-controller-name": "IdentityServerController",
+ "x-operation-name": "getConfig",
+ "tags": [
+ "IdentityServerController"
+ ],
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "description": "To get the openid configuration",
+ "responses": {
+ "200": {
+ "description": "OpenId Configuration",
+ "content": {}
+ },
+ "400": {
+ "description": "The syntax of the request entity is incorrect."
+ },
+ "401": {
+ "description": "Invalid Credentials."
+ },
+ "404": {
+ "description": "The entity requested does not exist."
+ },
+ "422": {
+ "description": "The syntax of the request entity is incorrect"
+ }
+ },
+ "operationId": "IdentityServerController.getConfig"
+ }
+ },
"/active-users/{range}": {
"get": {
"x-controller-name": "LoginActivityController",
@@ -1654,6 +1688,38 @@
"operationId": "LogoutController.cognitoLogout"
}
},
+ "/connect/auth": {
+ "post": {
+ "x-controller-name": "IdentityServerController",
+ "x-operation-name": "connectAuth",
+ "tags": [
+ "IdentityServerController"
+ ],
+ "description": "POST Call for idp login",
+ "responses": {
+ "200": {
+ "description": "Token Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TokenResponse"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/IdpAuthRequest"
+ }
+ }
+ }
+ },
+ "operationId": "IdentityServerController.connectAuth"
+ }
+ },
"/google/logout": {
"post": {
"x-controller-name": "LogoutController",
@@ -2055,7 +2121,6 @@
],
"additionalProperties": false
},
- "Function": {},
"AuthRefreshTokenRequest": {
"title": "AuthRefreshTokenRequest",
"type": "object",
@@ -2364,6 +2429,7 @@
],
"additionalProperties": false
},
+ "Function": {},
"ForgetPasswordDto": {
"title": "ForgetPasswordDto",
"type": "object",
@@ -2580,6 +2646,31 @@
],
"additionalProperties": false
},
+ "IdpAuthRequest": {
+ "title": "IdpAuthRequest",
+ "type": "object",
+ "description": "This is signature for idp authentication request.",
+ "properties": {
+ "client_id": {
+ "type": "string",
+ "description": "This property is supposed to be a string and is a required field"
+ },
+ "client_secret": {
+ "type": "string",
+ "description": "This property is supposed to be a string and is a required field"
+ },
+ "auth_method": {
+ "type": "string",
+ "description": "This property is supposed to be a string and is a required field"
+ }
+ },
+ "required": [
+ "client_id",
+ "client_secret",
+ "auth_method"
+ ],
+ "additionalProperties": false
+ },
"loopback.Count": {
"type": "object",
"title": "loopback.Count",
diff --git a/services/authentication-service/openapi.md b/services/authentication-service/openapi.md
index 9067143623..afdb350d34 100644
--- a/services/authentication-service/openapi.md
+++ b/services/authentication-service/openapi.md
@@ -30,6 +30,177 @@ Base URLs:
- HTTP Authentication, scheme: bearer
+
IdentityServerController
+
+## IdentityServerController.getConfig
+
+
+
+> Code samples
+
+```javascript
+
+const headers = {
+ 'Authorization':'Bearer {access-token}'
+};
+
+fetch('/.well-known/openid-configuration',
+{
+ method: 'GET',
+
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+```javascript--nodejs
+const fetch = require('node-fetch');
+
+const headers = {
+ 'Authorization':'Bearer {access-token}'
+};
+
+fetch('/.well-known/openid-configuration',
+{
+ method: 'GET',
+
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+`GET /.well-known/openid-configuration`
+
+To get the openid configuration
+
+> Example responses
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OpenId Configuration|None|
+|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|The syntax of the request entity is incorrect.|None|
+|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Invalid Credentials.|None|
+|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|The entity requested does not exist.|None|
+|422|[Unprocessable Entity](https://tools.ietf.org/html/rfc2518#section-10.3)|The syntax of the request entity is incorrect|None|
+
+Response Schema
+
+
+To perform this operation, you must be authenticated by means of one of the following methods:
+HTTPBearer
+
+
+## IdentityServerController.connectAuth
+
+
+
+> Code samples
+
+```javascript
+const inputBody = '{
+ "client_id": "string",
+ "client_secret": "string",
+ "auth_method": "string"
+}';
+const headers = {
+ 'Content-Type':'application/x-www-form-urlencoded',
+ 'Accept':'application/json'
+};
+
+fetch('/connect/auth',
+{
+ method: 'POST',
+ body: inputBody,
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+```javascript--nodejs
+const fetch = require('node-fetch');
+const inputBody = {
+ "client_id": "string",
+ "client_secret": "string",
+ "auth_method": "string"
+};
+const headers = {
+ 'Content-Type':'application/x-www-form-urlencoded',
+ 'Accept':'application/json'
+};
+
+fetch('/connect/auth',
+{
+ method: 'POST',
+ body: JSON.stringify(inputBody),
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+`POST /connect/auth`
+
+POST Call for idp login
+
+> Body parameter
+
+```yaml
+client_id: string
+client_secret: string
+auth_method: string
+
+```
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|body|body|[IdpAuthRequest](#schemaidpauthrequest)|false|none|
+
+> Example responses
+
+> 200 Response
+
+```json
+{
+ "accessToken": "string",
+ "refreshToken": "string",
+ "expires": 0,
+ "pubnubToken": "string"
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Token Response|[TokenResponse](#schematokenresponse)|
+
+
+This operation does not require authentication
+
+
LoginActivityController
## LoginActivityController.getActiveUsers
@@ -4376,22 +4547,6 @@ AuthTokenRequest
|code|string|true|none|none|
|clientId|string|true|none|none|
-Function
-
-
-
-
-
-
-```json
-null
-
-```
-
-### Properties
-
-*None*
-
AuthRefreshTokenRequest
@@ -4715,6 +4870,22 @@ AuthUser
|status|0|
|status|4|
+Function
+
+
+
+
+
+
+```json
+null
+
+```
+
+### Properties
+
+*None*
+
ForgetPasswordDto
@@ -4969,6 +5140,32 @@ ActiveUsersFilter
|userIdentity|string|true|none|none|
|userIdentifier|object|true|none|none|
+IdpAuthRequest
+
+
+
+
+
+
+```json
+{
+ "client_id": "string",
+ "client_secret": "string",
+ "auth_method": "string"
+}
+
+```
+
+IdpAuthRequest
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|client_id|string|true|none|This property is supposed to be a string and is a required field|
+|client_secret|string|true|none|This property is supposed to be a string and is a required field|
+|auth_method|string|true|none|This property is supposed to be a string and is a required field|
+
loopback.Count
diff --git a/services/authentication-service/package.json b/services/authentication-service/package.json
index 9f3fdf9f54..0589fe2be8 100644
--- a/services/authentication-service/package.json
+++ b/services/authentication-service/package.json
@@ -25,6 +25,7 @@
}
},
"scripts": {
+ "start": "node -r source-map-support/register .",
"prebuild": "npm run clean",
"build": "lb-tsc && npm run openapi-spec && npm run apidocs",
"build:watch": "lb-tsc --watch",
diff --git a/services/authentication-service/src/component.ts b/services/authentication-service/src/component.ts
index 843256c372..9647651bed 100644
--- a/services/authentication-service/src/component.ts
+++ b/services/authentication-service/src/component.ts
@@ -120,6 +120,7 @@ import {repositories as sequelizeRepositories} from './repositories/sequelize';
import {MySequence} from './sequence';
import {
ActiveUserFilterBuilderService,
+ IdpLoginService,
LoginActivityHelperService,
LoginHelperService,
OtpService,
@@ -193,6 +194,7 @@ export class AuthenticationServiceComponent implements Component {
this.application
.bind('services.loginActivityHelperService')
.toClass(LoginActivityHelperService);
+ this.application.bind('services.IdpLoginService').toClass(IdpLoginService);
//set the userActivity to false by default
this.application
diff --git a/services/authentication-service/src/controllers/index.ts b/services/authentication-service/src/controllers/index.ts
index 748ae6cde0..2592f71bc1 100644
--- a/services/authentication-service/src/controllers/index.ts
+++ b/services/authentication-service/src/controllers/index.ts
@@ -9,6 +9,7 @@ import {
CognitoLoginController,
FacebookLoginController,
GoogleLoginController,
+ IdentityServerController,
InstagramLoginController,
KeycloakLoginController,
LoginController,
@@ -43,4 +44,5 @@ export const controllers = [
SamlLoginController,
Auth0LoginController,
LoginActivityController,
+ IdentityServerController,
];
diff --git a/services/authentication-service/src/modules/auth/controllers/apple-login.controller.ts b/services/authentication-service/src/modules/auth/controllers/apple-login.controller.ts
index 961eb24766..6601e7f00c 100644
--- a/services/authentication-service/src/modules/auth/controllers/apple-login.controller.ts
+++ b/services/authentication-service/src/modules/auth/controllers/apple-login.controller.ts
@@ -33,6 +33,7 @@ import {authorize} from 'loopback4-authorization';
import {URLSearchParams} from 'url';
import {AuthCodeBindings, AuthCodeGeneratorFn} from '../../../providers';
import {AuthClientRepository} from '../../../repositories';
+import {IdpLoginService} from '../../../services';
import {AuthUser, ClientAuthRequest, TokenResponse} from '../models';
const queryGen = (from: 'body' | 'query') => {
@@ -50,22 +51,11 @@ export class AppleLoginController {
@inject(LOGGER.LOGGER_INJECT) public logger: ILogger,
@inject(AuthCodeBindings.AUTH_CODE_GENERATOR_PROVIDER)
private readonly getAuthCode: AuthCodeGeneratorFn,
- ) {}
+ @inject('services.IdpLoginService')
+ private readonly idpLoginService: IdpLoginService,
+ ) { }
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
- @authenticate(
- STRATEGY.APPLE_OAUTH2,
- {
- accessType: 'offline',
- scope: ['name', 'email'],
- callbackURL: process.env.APPLE_AUTH_CALLBACK_URL,
- clientID: process.env.APPLE_AUTH_CLIENT_ID,
- teamID: process.env.APPLE_AUTH_TEAM_ID,
- keyID: process.env.APPLE_AUTH_KEY_ID,
- privateKeyLocation: process.env.APPLE_AUTH_PRIVATE_KEY_LOCATION,
- },
- queryGen('body'),
- )
@authorize({permissions: ['*']})
@post('/auth/oauth-apple', {
responses: {
@@ -84,8 +74,8 @@ export class AppleLoginController {
},
})
clientCreds: ClientAuthRequest,
- ): void {
- //do nothing
+ ): Promise {
+ return this.idpLoginService.loginViaApple();
}
@authenticate(
@@ -138,8 +128,7 @@ export class AppleLoginController {
const token = await this.getAuthCode(client, user);
const role = user.role;
response.redirect(
- `${process.env.WEBAPP_URL ?? ''}${
- client.redirectUrl
+ `${process.env.WEBAPP_URL ?? ''}${client.redirectUrl
}?code=${token}&role=${role}`,
);
} catch (error) {
diff --git a/services/authentication-service/src/modules/auth/controllers/azure-login.controller.ts b/services/authentication-service/src/modules/auth/controllers/azure-login.controller.ts
index 8b54bb74ec..8432586a99 100644
--- a/services/authentication-service/src/modules/auth/controllers/azure-login.controller.ts
+++ b/services/authentication-service/src/modules/auth/controllers/azure-login.controller.ts
@@ -29,6 +29,7 @@ import {
import {authorize} from 'loopback4-authorization';
import {AuthCodeBindings, AuthCodeGeneratorFn} from '../../../providers';
import {AuthClientRepository} from '../../../repositories';
+import {IdpLoginService} from '../../../services';
import {AuthUser, ClientAuthRequest, TokenResponse} from '../models';
const queryGen = (from: 'body' | 'query') => {
@@ -49,44 +50,11 @@ export class AzureLoginController {
@inject(LOGGER.LOGGER_INJECT) public logger: ILogger,
@inject(AuthCodeBindings.AUTH_CODE_GENERATOR_PROVIDER)
private readonly getAuthCode: AuthCodeGeneratorFn,
- ) {}
+ @inject('services.IdpLoginService')
+ private readonly idpLoginService: IdpLoginService,
+ ) { }
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
- @authenticate(
- STRATEGY.AZURE_AD,
- {
- scope: ['profile', 'email', 'openid', 'offline_access'],
- identityMetadata: process.env.AZURE_IDENTITY_METADATA,
- clientID: process.env.AZURE_AUTH_CLIENT_ID,
- responseType: 'code',
- responseMode: 'query',
- redirectUrl: process.env.AZURE_AUTH_REDIRECT_URL,
- clientSecret: process.env.AZURE_AUTH_CLIENT_SECRET,
- allowHttpForRedirectUrl: !!+(
- process.env.AZURE_AUTH_ALLOW_HTTP_REDIRECT ?? 1
- ),
- passReqToCallback: !!+(process.env.AZURE_AUTH_PASS_REQ_CALLBACK ?? 0),
- validateIssuer: !!+(process.env.AZURE_AUTH_VALIDATE_ISSUER ?? 1),
- useCookieInsteadOfSession: !!+(
- process.env.AZURE_AUTH_COOKIE_INSTEAD_SESSION ?? 1
- ),
- cookieEncryptionKeys: [
- {
- key: process.env.AZURE_AUTH_COOKIE_KEY,
- iv: process.env.AZURE_AUTH_COOKIE_IV,
- },
- ],
- isB2c: !!+(process.env.AZURE_AUTH_B2C_TENANT ?? 0),
- clockSkew: +(process.env.AZURE_AUTH_CLOCK_SKEW ?? clockSkew),
- loggingLevel: process.env.AZURE_AUTH_LOG_LEVEL,
- loggingNoPII: !!+(process.env.AZURE_AUTH_LOG_PII ?? 1),
- nonceLifetime: +(process.env.AZURE_AUTH_NONCE_TIME ?? nonceTime),
- nonceMaxAmount: +(process.env.AZURE_AUTH_NONCE_COUNT ?? nonceCount),
- issuer: process.env.AZURE_AUTH_ISSUER,
- cookieSameSite: !!+(process.env.AZURE_AUTH_COOKIE_SAME_SITE ?? 0),
- },
- queryGen('query'),
- )
@authorize({permissions: ['*']})
@oas.deprecated()
@get('/auth/azure', {
@@ -108,7 +76,7 @@ export class AzureLoginController {
@param.query.string('client_secret')
clientSecret?: string, //NOSONAR
): Promise {
- //do nothing
+ return this.idpLoginService.loginViaAzure();
}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
@@ -247,8 +215,7 @@ export class AzureLoginController {
const token = await this.getAuthCode(client, user);
const role = user.role;
response.redirect(
- `${process.env.WEBAPP_URL ?? ''}${
- client.redirectUrl
+ `${process.env.WEBAPP_URL ?? ''}${client.redirectUrl
}?code=${token}&role=${role}`,
);
} catch (error) {
diff --git a/services/authentication-service/src/modules/auth/controllers/cognito-login.controller.ts b/services/authentication-service/src/modules/auth/controllers/cognito-login.controller.ts
index 294489aacf..0293d210af 100644
--- a/services/authentication-service/src/modules/auth/controllers/cognito-login.controller.ts
+++ b/services/authentication-service/src/modules/auth/controllers/cognito-login.controller.ts
@@ -34,6 +34,7 @@ import {authorize} from 'loopback4-authorization';
import {URLSearchParams} from 'url';
import {AuthCodeBindings, AuthCodeGeneratorFn} from '../../../providers';
import {AuthClientRepository} from '../../../repositories';
+import {IdpLoginService} from '../../../services';
import {AuthUser, ClientAuthRequest, TokenResponse} from '../models';
const queryGen = (from: 'body' | 'query') => {
@@ -51,20 +52,11 @@ export class CognitoLoginController {
@inject(LOGGER.LOGGER_INJECT) public logger: ILogger,
@inject(AuthCodeBindings.AUTH_CODE_GENERATOR_PROVIDER)
private readonly getAuthCode: AuthCodeGeneratorFn,
- ) {}
+ @inject('services.IdpLoginService')
+ private readonly idpLoginService: IdpLoginService,
+ ) { }
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
- @authenticate(
- STRATEGY.COGNITO_OAUTH2,
- {
- callbackURL: process.env.COGNITO_AUTH_CALLBACK_URL,
- clientDomain: process.env.COGNITO_AUTH_CLIENT_DOMAIN,
- clientID: process.env.COGNITO_AUTH_CLIENT_ID,
- clientSecret: process.env.COGNITO_AUTH_CLIENT_SECRET,
- region: process.env.COGNITO_AUTH_REGION,
- },
- queryGen('query'),
- )
@authorize({permissions: ['*']})
@oas.deprecated()
@get('/auth/cognito', {
@@ -91,21 +83,10 @@ export class CognitoLoginController {
@param.query.string('client_secret')
clientSecret?: string,
): Promise {
- //do nothing
+ return this.idpLoginService.loginViaCognito();
}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
- @authenticate(
- STRATEGY.COGNITO_OAUTH2,
- {
- callbackURL: process.env.COGNITO_AUTH_CALLBACK_URL,
- clientDomain: process.env.COGNITO_AUTH_CLIENT_DOMAIN,
- clientID: process.env.COGNITO_AUTH_CLIENT_ID,
- clientSecret: process.env.COGNITO_AUTH_CLIENT_SECRET,
- region: process.env.COGNITO_AUTH_REGION,
- },
- queryGen('body'),
- )
@authorize({permissions: ['*']})
@post('/auth/cognito', {
responses: {
@@ -129,7 +110,7 @@ export class CognitoLoginController {
})
clientCreds?: ClientAuthRequest,
): Promise {
- //do nothing
+ return this.idpLoginService.loginViaCognito();
}
@authenticate(
diff --git a/services/authentication-service/src/modules/auth/controllers/facebook-login.controller.ts b/services/authentication-service/src/modules/auth/controllers/facebook-login.controller.ts
index 895c367525..305164a412 100644
--- a/services/authentication-service/src/modules/auth/controllers/facebook-login.controller.ts
+++ b/services/authentication-service/src/modules/auth/controllers/facebook-login.controller.ts
@@ -33,6 +33,7 @@ import {authorize} from 'loopback4-authorization';
import {URLSearchParams} from 'url';
import {AuthCodeBindings, AuthCodeGeneratorFn} from '../../../providers';
import {AuthClientRepository} from '../../../repositories';
+import {IdpLoginService} from '../../../services';
import {AuthUser, ClientAuthRequest, TokenResponse} from '../models';
const queryGen = (from: 'body' | 'query') => {
@@ -50,21 +51,11 @@ export class FacebookLoginController {
@inject(LOGGER.LOGGER_INJECT) public logger: ILogger,
@inject(AuthCodeBindings.AUTH_CODE_GENERATOR_PROVIDER)
private readonly getAuthCode: AuthCodeGeneratorFn,
- ) {}
+ @inject('services.IdpLoginService')
+ private readonly idpLoginService: IdpLoginService,
+ ) { }
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
- @authenticate(
- STRATEGY.FACEBOOK_OAUTH2,
- {
- accessType: 'offline',
- authorizationURL: process.env.FACEBOOK_AUTH_URL,
- callbackURL: process.env.FACEBOOK_AUTH_CALLBACK_URL,
- clientID: process.env.FACEBOOK_AUTH_CLIENT_ID,
- clientSecret: process.env.FACEBOOK_AUTH_CLIENT_SECRET,
- tokenURL: process.env.FACEBOOK_AUTH_TOKEN_URL,
- },
- queryGen('body'),
- )
@authorize({permissions: ['*']})
@post('/auth/facebook', {
responses: {
@@ -88,7 +79,7 @@ export class FacebookLoginController {
})
clientCreds?: ClientAuthRequest,
): Promise {
- //do nothing
+ return this.idpLoginService.loginViaFacebook();
}
@authenticate(
diff --git a/services/authentication-service/src/modules/auth/controllers/google-login.controller.ts b/services/authentication-service/src/modules/auth/controllers/google-login.controller.ts
index 0489390da5..cf61a609c0 100644
--- a/services/authentication-service/src/modules/auth/controllers/google-login.controller.ts
+++ b/services/authentication-service/src/modules/auth/controllers/google-login.controller.ts
@@ -34,6 +34,7 @@ import {authorize} from 'loopback4-authorization';
import {URLSearchParams} from 'url';
import {AuthCodeBindings, AuthCodeGeneratorFn} from '../../../providers';
import {AuthClientRepository} from '../../../repositories';
+import {IdpLoginService} from '../../../services';
import {AuthUser, ClientAuthRequest, TokenResponse} from '../models';
const queryGen = (from: 'body' | 'query') => {
@@ -51,29 +52,18 @@ export class GoogleLoginController {
@inject(LOGGER.LOGGER_INJECT) public logger: ILogger,
@inject(AuthCodeBindings.AUTH_CODE_GENERATOR_PROVIDER)
private readonly getAuthCode: AuthCodeGeneratorFn,
- ) {}
+ @inject('services.IdpLoginService')
+ private readonly idpLoginService: IdpLoginService,
+ ) { }
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
- @authenticate(
- STRATEGY.GOOGLE_OAUTH2,
- {
- accessType: 'offline',
- scope: ['profile', 'email'],
- authorizationURL: process.env.GOOGLE_AUTH_URL,
- callbackURL: process.env.GOOGLE_AUTH_CALLBACK_URL,
- clientID: process.env.GOOGLE_AUTH_CLIENT_ID,
- clientSecret: process.env.GOOGLE_AUTH_CLIENT_SECRET,
- tokenURL: process.env.GOOGLE_AUTH_TOKEN_URL,
- },
- queryGen('query'),
- )
@authorize({permissions: ['*']})
@oas.deprecated()
@get('/auth/google', {
responses: {
[STATUS_CODE.OK]: {
description: `Google Token Response,
- (Deprecated: Possible security issue if secret is passed via query params,
+ (Deprecated: Possible security issue if secret is passed via query params,
please use the post endpoint)`,
content: {
[CONTENT_TYPE.JSON]: {
@@ -94,23 +84,10 @@ export class GoogleLoginController {
@param.query.string('client_secret')
clientSecret?: string,
): Promise {
- //do nothing
+ return this.idpLoginService.loginViaGoogle();
}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
- @authenticate(
- STRATEGY.GOOGLE_OAUTH2,
- {
- accessType: 'offline',
- scope: ['profile', 'email'],
- authorizationURL: process.env.GOOGLE_AUTH_URL,
- callbackURL: process.env.GOOGLE_AUTH_CALLBACK_URL,
- clientID: process.env.GOOGLE_AUTH_CLIENT_ID,
- clientSecret: process.env.GOOGLE_AUTH_CLIENT_SECRET,
- tokenURL: process.env.GOOGLE_AUTH_TOKEN_URL,
- },
- queryGen('body'),
- )
@authorize({permissions: ['*']})
@post('/auth/google', {
responses: {
@@ -134,7 +111,7 @@ export class GoogleLoginController {
})
clientCreds?: ClientAuthRequest,
): Promise {
- //do nothing
+ return this.idpLoginService.loginViaGoogle();
}
@authenticate(
diff --git a/services/authentication-service/src/modules/auth/controllers/identity-server.controller.ts b/services/authentication-service/src/modules/auth/controllers/identity-server.controller.ts
new file mode 100644
index 0000000000..6c0c3102e2
--- /dev/null
+++ b/services/authentication-service/src/modules/auth/controllers/identity-server.controller.ts
@@ -0,0 +1,264 @@
+import {inject} from '@loopback/core';
+import {repository} from '@loopback/repository';
+import {
+ get,
+ getModelSchemaRef,
+ HttpErrors,
+ param,
+ post,
+ requestBody,
+} from '@loopback/rest';
+import {
+ AuthenticateErrorKeys,
+ CONTENT_TYPE,
+ ErrorCodes,
+ OPERATION_SECURITY_SPEC,
+ RevokedTokenRepository,
+ STATUS_CODE,
+ SuccessResponse,
+ X_TS_TYPE,
+} from '@sourceloop/core';
+import {
+ authenticate,
+ authenticateClient,
+ AuthenticationBindings,
+ AuthErrorKeys,
+ STRATEGY,
+} from 'loopback4-authentication';
+import {authorize} from 'loopback4-authorization';
+import {
+ AuthCodeBindings,
+ AuthServiceBindings,
+ CodeReaderFn,
+ IUserActivity,
+ LoginType,
+ RefreshTokenRepository,
+ RefreshTokenRequest,
+ UserRepository,
+ UserTenantRepository,
+} from '../../..';
+import {IdpLoginService} from '../../../services';
+import {
+ AuthTokenRequest,
+ AuthUser,
+ IdpAuthMethod,
+ IdpAuthRequest,
+ IdpConfiguration,
+ TokenResponse,
+} from '../models';
+
+export class IdentityServerController {
+ constructor(
+ @inject(AuthCodeBindings.CODEREADER_PROVIDER)
+ private readonly codeReader: CodeReaderFn,
+ @inject('services.IdpLoginService')
+ private readonly idpLoginService: IdpLoginService,
+ @inject(AuthenticationBindings.CURRENT_USER)
+ private readonly user: AuthUser | undefined,
+ @repository(RevokedTokenRepository)
+ private readonly revokedTokens: RevokedTokenRepository,
+ @repository(RefreshTokenRepository)
+ public refreshTokenRepo: RefreshTokenRepository,
+ @repository(UserRepository)
+ public userRepo: UserRepository,
+ @repository(UserTenantRepository)
+ public userTenantRepo: UserTenantRepository,
+ @inject(AuthServiceBindings.MarkUserActivity, {optional: true})
+ private readonly userActivity?: IUserActivity,
+ ) { }
+
+ @authenticateClient(STRATEGY.CLIENT_PASSWORD)
+ @authorize({permissions: ['*']})
+ @post('/connect/auth', {
+ description: 'POST Call for idp login',
+ responses: {
+ [STATUS_CODE.OK]: {
+ description: 'Token Response',
+ content: {
+ [CONTENT_TYPE.JSON]: {
+ schema: {[X_TS_TYPE]: TokenResponse},
+ },
+ },
+ },
+ },
+ })
+ async connectAuth(
+ @requestBody({
+ content: {
+ [CONTENT_TYPE.FORM_URLENCODED]: {
+ schema: getModelSchemaRef(IdpAuthRequest),
+ },
+ },
+ })
+ idpAuthRequest: IdpAuthRequest,
+ ): Promise {
+ switch (idpAuthRequest?.auth_method) {
+ case IdpAuthMethod.COGNITO:
+ await this.idpLoginService.loginViaCognito();
+ break;
+ case IdpAuthMethod.GOOGLE:
+ await this.idpLoginService.loginViaGoogle();
+ break;
+ case IdpAuthMethod.SAML:
+ await this.idpLoginService.loginViaSaml();
+ break;
+ case IdpAuthMethod.FACEBOOK:
+ await this.idpLoginService.loginViaFacebook();
+ break;
+ case IdpAuthMethod.APPLE:
+ await this.idpLoginService.loginViaApple();
+ break;
+ case IdpAuthMethod.AZURE:
+ await this.idpLoginService.loginViaAzure();
+ break;
+ case IdpAuthMethod.INSTAGRAM:
+ await this.idpLoginService.loginViaInstagram();
+ break;
+ case IdpAuthMethod.KEYCLOAK:
+ await this.idpLoginService.loginViaKeycloak();
+ break;
+ }
+ }
+
+ @authorize({permissions: ['*']})
+ @get('/.well-known/openid-configuration', {
+ security: OPERATION_SECURITY_SPEC,
+ description: 'To get the openid configuration',
+ responses: {
+ [STATUS_CODE.OK]: {
+ description: 'OpenId Configuration',
+ content: {
+ [CONTENT_TYPE.JSON]: IdpConfiguration,
+ },
+ },
+ ...ErrorCodes,
+ },
+ })
+ async getConfig(): Promise {
+ this.idpLoginService.getOpenIdConfiguration();
+ }
+
+ @authorize({permissions: ['*']})
+ @post('/connect/token', {
+ description:
+ 'Send the code received from the POST /auth/login api and get refresh token and access token (webapps)',
+ responses: {
+ [STATUS_CODE.OK]: {
+ description: 'Token Response',
+ content: {
+ [CONTENT_TYPE.JSON]: {
+ schema: {[X_TS_TYPE]: TokenResponse},
+ },
+ },
+ },
+ ...ErrorCodes,
+ },
+ })
+ async getToken(@requestBody() req: AuthTokenRequest): Promise {
+ return this.idpLoginService.generateToken(req);
+ }
+
+ @authenticate(STRATEGY.BEARER, {
+ passReqToCallback: true,
+ })
+ @authorize({permissions: ['*']})
+ @get('/connect/userinfo', {
+ security: OPERATION_SECURITY_SPEC,
+ description: 'To get the user details',
+ responses: {
+ [STATUS_CODE.OK]: {
+ description: 'User Object',
+ content: {
+ [CONTENT_TYPE.JSON]: AuthUser,
+ },
+ },
+ ...ErrorCodes,
+ },
+ })
+ async me(): Promise {
+ if (!this.user) {
+ throw new HttpErrors.Unauthorized(AuthErrorKeys.TokenInvalid);
+ }
+ delete this.user.deviceInfo;
+ return new AuthUser(this.user);
+ }
+
+ @authenticate(STRATEGY.BEARER, {
+ passReqToCallback: true,
+ })
+ @authorize({permissions: ['*']})
+ @post('/connect/endsession', {
+ security: OPERATION_SECURITY_SPEC,
+ description: 'To logout',
+ responses: {
+ [STATUS_CODE.OK]: {
+ description: 'Success Response',
+ content: {
+ [CONTENT_TYPE.JSON]: {
+ schema: {[X_TS_TYPE]: SuccessResponse},
+ },
+ },
+ },
+ ...ErrorCodes,
+ },
+ })
+ async logout(
+ @param.header.string('Authorization', {
+ description:
+ 'This is the access token which is required to authenticate user.',
+ })
+ auth: string,
+ @requestBody({
+ content: {
+ [CONTENT_TYPE.JSON]: {
+ schema: getModelSchemaRef(RefreshTokenRequest, {
+ partial: true,
+ }),
+ },
+ },
+ })
+ req: RefreshTokenRequest,
+ ): Promise {
+ const token = auth?.replace(/bearer /i, '');
+ if (!token || !req.refreshToken) {
+ throw new HttpErrors.UnprocessableEntity(
+ AuthenticateErrorKeys.TokenMissing,
+ );
+ }
+
+ const refreshTokenModel = await this.refreshTokenRepo.get(req.refreshToken);
+ if (!refreshTokenModel) {
+ throw new HttpErrors.Unauthorized(AuthErrorKeys.TokenExpired);
+ }
+ if (refreshTokenModel.accessToken !== token) {
+ throw new HttpErrors.Unauthorized(AuthErrorKeys.TokenInvalid);
+ }
+ await this.revokedTokens.set(token, {token});
+ await this.refreshTokenRepo.delete(req.refreshToken);
+ if (refreshTokenModel.pubnubToken) {
+ await this.refreshTokenRepo.delete(refreshTokenModel.pubnubToken);
+ }
+
+ const user = await this.userRepo.findById(refreshTokenModel.userId);
+
+ const userTenant = await this.userTenantRepo.findOne({
+ where: {userId: user.id},
+ });
+
+ if (this.userActivity?.markUserActivity)
+ this.idpLoginService.markUserActivity(
+ user,
+ userTenant,
+ {
+ ...user,
+ clientId: refreshTokenModel.clientId,
+ },
+ LoginType.LOGOUT,
+ );
+ return new SuccessResponse({
+ success: true,
+
+ key: refreshTokenModel.userId,
+ });
+ }
+}
diff --git a/services/authentication-service/src/modules/auth/controllers/index.ts b/services/authentication-service/src/modules/auth/controllers/index.ts
index dd16480d76..d56c66d028 100644
--- a/services/authentication-service/src/modules/auth/controllers/index.ts
+++ b/services/authentication-service/src/modules/auth/controllers/index.ts
@@ -4,6 +4,7 @@ export * from './azure-login.controller';
export * from './cognito-login.controller';
export * from './facebook-login.controller';
export * from './google-login.controller';
+export * from './identity-server.controller';
export * from './instagram-login.controller';
export * from './keycloak-login.controller';
export * from './login.controller';
diff --git a/services/authentication-service/src/modules/auth/controllers/instagram-login.controller.ts b/services/authentication-service/src/modules/auth/controllers/instagram-login.controller.ts
index b2adbdbc5f..a879f3459f 100644
--- a/services/authentication-service/src/modules/auth/controllers/instagram-login.controller.ts
+++ b/services/authentication-service/src/modules/auth/controllers/instagram-login.controller.ts
@@ -33,6 +33,7 @@ import {authorize} from 'loopback4-authorization';
import {URLSearchParams} from 'url';
import {AuthCodeBindings, AuthCodeGeneratorFn} from '../../../providers';
import {AuthClientRepository} from '../../../repositories';
+import {IdpLoginService} from '../../../services';
import {AuthUser, ClientAuthRequest, TokenResponse} from '../models';
const queryGen = (from: 'body' | 'query') => {
@@ -50,21 +51,11 @@ export class InstagramLoginController {
@inject(LOGGER.LOGGER_INJECT) public logger: ILogger,
@inject(AuthCodeBindings.AUTH_CODE_GENERATOR_PROVIDER)
private readonly getAuthCode: AuthCodeGeneratorFn,
- ) {}
+ @inject('services.IdpLoginService')
+ private readonly idpLoginService: IdpLoginService,
+ ) { }
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
- @authenticate(
- STRATEGY.INSTAGRAM_OAUTH2,
- {
- accessType: 'offline',
- authorizationURL: process.env.INSTAGRAM_AUTH_URL,
- callbackURL: process.env.INSTAGRAM_AUTH_CALLBACK_URL,
- clientID: process.env.INSTAGRAM_AUTH_CLIENT_ID,
- clientSecret: process.env.INSTAGRAM_AUTH_CLIENT_SECRET,
- tokenURL: process.env.INSTAGRAM_AUTH_TOKEN_URL,
- },
- queryGen('body'),
- )
@authorize({permissions: ['*']})
@post('/auth/instagram', {
responses: {
@@ -88,7 +79,7 @@ export class InstagramLoginController {
})
clientCreds?: ClientAuthRequest,
): Promise {
- //do nothing
+ return this.idpLoginService.loginViaInstagram();
}
@authenticate(
diff --git a/services/authentication-service/src/modules/auth/controllers/keycloak-login.controller.ts b/services/authentication-service/src/modules/auth/controllers/keycloak-login.controller.ts
index c85378c5b8..f0a9755dd5 100644
--- a/services/authentication-service/src/modules/auth/controllers/keycloak-login.controller.ts
+++ b/services/authentication-service/src/modules/auth/controllers/keycloak-login.controller.ts
@@ -34,6 +34,7 @@ import {authorize} from 'loopback4-authorization';
import {URLSearchParams} from 'url';
import {AuthCodeBindings, AuthCodeGeneratorFn} from '../../../providers';
import {AuthClientRepository} from '../../../repositories';
+import {IdpLoginService} from '../../../services';
import {AuthUser, ClientAuthRequest, TokenResponse} from '../models';
const queryGen = (from: 'body' | 'query') => {
@@ -51,25 +52,11 @@ export class KeycloakLoginController {
@inject(LOGGER.LOGGER_INJECT) public logger: ILogger,
@inject(AuthCodeBindings.AUTH_CODE_GENERATOR_PROVIDER)
private readonly getAuthCode: AuthCodeGeneratorFn,
- ) {}
+ @inject('services.IdpLoginService')
+ private readonly idpLoginService: IdpLoginService,
+ ) { }
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
- @authenticate(
- STRATEGY.KEYCLOAK,
- {
- host: process.env.KEYCLOAK_HOST,
- realm: process.env.KEYCLOAK_REALM, //'Tenant1',
- clientID: process.env.KEYCLOAK_CLIENT_ID, //'onboarding',
- clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
- //'e607fd75-adc8-4af7-9f03-c9e79a4b8b72',
- callbackURL: process.env.KEYCLOAK_CALLBACK_URL,
- //'http://localhost:3001/auth/keycloak-auth-redirect',
- authorizationURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`,
- tokenURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
- userInfoURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/userinfo`,
- },
- queryGen('body'),
- )
@authorize({permissions: ['*']})
@post('/auth/keycloak', {
description: 'POST Call for keycloak based login',
@@ -94,26 +81,10 @@ export class KeycloakLoginController {
})
clientCreds?: ClientAuthRequest, //NOSONAR
): Promise {
- //do nothing
+ return this.idpLoginService.loginViaKeycloak();
}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
- @authenticate(
- STRATEGY.KEYCLOAK,
- {
- host: process.env.KEYCLOAK_HOST,
- realm: process.env.KEYCLOAK_REALM, //'Tenant1',
- clientID: process.env.KEYCLOAK_CLIENT_ID, //'onboarding',
- clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
- //'e607fd75-adc8-4af7-9f03-c9e79a4b8b72',
- callbackURL: process.env.KEYCLOAK_CALLBACK_URL,
- //'http://localhost:3001/auth/keycloak-auth-redirect',
- authorizationURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`,
- tokenURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
- userInfoURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/userinfo`,
- },
- queryGen('query'),
- )
@authorize({permissions: ['*']})
@oas.deprecated()
@get('/auth/keycloak', {
@@ -136,7 +107,7 @@ export class KeycloakLoginController {
@param.query.string('client_secret')
clientSecret?: string, //NOSONAR
): Promise {
- //do nothing
+ return this.idpLoginService.loginViaKeycloak();
}
@authenticate(
diff --git a/services/authentication-service/src/modules/auth/controllers/login.controller.ts b/services/authentication-service/src/modules/auth/controllers/login.controller.ts
index 682375f780..1a51778bac 100644
--- a/services/authentication-service/src/modules/auth/controllers/login.controller.ts
+++ b/services/authentication-service/src/modules/auth/controllers/login.controller.ts
@@ -51,7 +51,6 @@ import {
import {
AuthCodeBindings,
AuthCodeGeneratorFn,
- CodeReaderFn,
JWTSignerFn,
JWTVerifierFn,
JwtPayloadFn,
@@ -71,7 +70,7 @@ import {
UserRepository,
UserTenantRepository,
} from '../../../repositories';
-import {LoginHelperService} from '../../../services';
+import {IdpLoginService, LoginHelperService} from '../../../services';
import {
ActorId,
ExternalTokens,
@@ -121,6 +120,8 @@ export class LoginController {
private readonly getJwtPayload: JwtPayloadFn,
@inject('services.LoginHelperService')
private readonly loginHelperService: LoginHelperService,
+ @inject('services.IdpLoginService')
+ private readonly idpLoginService: IdpLoginService,
@inject(AuthCodeBindings.AUTH_CODE_GENERATOR_PROVIDER)
private readonly getAuthCode: AuthCodeGeneratorFn,
@inject(AuthCodeBindings.JWT_SIGNER)
@@ -254,46 +255,8 @@ export class LoginController {
...ErrorCodes,
},
})
- async getToken(
- @requestBody() req: AuthTokenRequest,
- @inject(AuthCodeBindings.CODEREADER_PROVIDER)
- codeReader: CodeReaderFn,
- ): Promise {
- const authClient = await this.authClientRepository.findOne({
- where: {
- clientId: req.clientId,
- },
- });
- if (!authClient) {
- throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
- }
- try {
- const code = await codeReader(req.code);
- const payload = (await this.jwtVerifier(code, {
- audience: req.clientId,
- })) as ClientAuthCode;
- if (payload.mfa) {
- throw new HttpErrors.Unauthorized(AuthErrorKeys.UserVerificationFailed);
- }
-
- if (
- payload.userId &&
- !(await this.userRepo.firstTimeUser(payload.userId))
- ) {
- await this.userRepo.updateLastLogin(payload.userId);
- }
-
- return await this.createJWT(payload, authClient, LoginType.ACCESS);
- } catch (error) {
- this.logger.error(error);
- if (error.name === 'TokenExpiredError') {
- throw new HttpErrors.Unauthorized(AuthErrorKeys.CodeExpired);
- } else if (HttpErrors.HttpError.prototype.isPrototypeOf(error)) {
- throw error;
- } else {
- throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
- }
- }
+ async getToken(@requestBody() req: AuthTokenRequest): Promise {
+ return this.idpLoginService.generateToken(req);
}
@authorize({permissions: ['*']})
diff --git a/services/authentication-service/src/modules/auth/controllers/saml-login.controller.ts b/services/authentication-service/src/modules/auth/controllers/saml-login.controller.ts
index 4b2390d2db..dfc4a33cbf 100644
--- a/services/authentication-service/src/modules/auth/controllers/saml-login.controller.ts
+++ b/services/authentication-service/src/modules/auth/controllers/saml-login.controller.ts
@@ -27,6 +27,7 @@ import {
import {authorize} from 'loopback4-authorization';
import {AuthCodeBindings, AuthCodeGeneratorFn} from '../../../providers';
import {AuthClientRepository} from '../../../repositories';
+import {IdpLoginService} from '../../../services';
import {AuthUser, ClientAuthRequest, TokenResponse} from '../models';
export class SamlLoginController {
@@ -37,23 +38,11 @@ export class SamlLoginController {
public logger: ILogger,
@inject(AuthCodeBindings.AUTH_CODE_GENERATOR_PROVIDER)
private readonly getAuthCode: AuthCodeGeneratorFn,
- ) {}
+ @inject('services.IdpLoginService')
+ private readonly idpLoginService: IdpLoginService,
+ ) { }
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
- @authenticate(STRATEGY.SAML, {
- accessType: 'offline',
- scope: ['profile', 'email'],
- callbackURL: process.env.SAML_CALLBACK_URL,
- issuer: process.env.SAML_ISSUER,
- cert: process.env.SAML_CERT,
- entryPoint: process.env.SAML_ENTRY_POINT,
- audience: process.env.SAML_AUDIENCE,
- logoutUrl: process.env.SAML_LOGOUT_URL,
- passReqToCallback: !!+(process.env.SAML_AUTH_PASS_REQ_CALLBACK ?? 0),
- validateInResponseTo: !!+(process.env.VALIDATE_RESPONSE ?? 1),
- idpIssuer: process.env.IDP_ISSUER,
- logoutCallbackUrl: process.env.SAML_LOGOUT_CALLBACK_URL,
- })
@authorize({permissions: ['*']})
@post('/auth/saml', {
description: 'POST Call for saml based login',
@@ -78,7 +67,7 @@ export class SamlLoginController {
})
clientCreds?: ClientAuthRequest, //NOSONAR
): Promise {
- //do nothing
+ return this.idpLoginService.loginViaSaml();
}
@authenticate(STRATEGY.SAML, {
diff --git a/services/authentication-service/src/modules/auth/models/idp-auth-request.dto.ts b/services/authentication-service/src/modules/auth/models/idp-auth-request.dto.ts
new file mode 100644
index 0000000000..7d06733c11
--- /dev/null
+++ b/services/authentication-service/src/modules/auth/models/idp-auth-request.dto.ts
@@ -0,0 +1,36 @@
+// Copyright (c) 2023 Sourcefuse Technologies
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+/* eslint-disable @typescript-eslint/naming-convention */
+
+import {model, property} from '@loopback/repository';
+import {CoreModel} from '@sourceloop/core';
+import {IdpAuthMethod} from './idp-auth.method';
+import {ModelPropertyDescriptionString} from './model-property-description.enum';
+
+@model({
+ description: 'This is signature for idp authentication request.',
+})
+export class IdpAuthRequest extends CoreModel {
+ @property({
+ type: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ client_id: string; //NOSONAR
+
+ @property({
+ type: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ client_secret: string; //NOSONAR
+
+ @property({
+ type: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ auth_method: IdpAuthMethod; //NOSONAR
+}
diff --git a/services/authentication-service/src/modules/auth/models/idp-auth.method.ts b/services/authentication-service/src/modules/auth/models/idp-auth.method.ts
new file mode 100644
index 0000000000..f7b1cc30ba
--- /dev/null
+++ b/services/authentication-service/src/modules/auth/models/idp-auth.method.ts
@@ -0,0 +1,10 @@
+export enum IdpAuthMethod {
+ COGNITO = 'COGNITO',
+ GOOGLE = 'GOOGLE',
+ SAML = 'SAML',
+ FACEBOOK = 'FACEBOOK',
+ APPLE = 'APPLE',
+ AZURE = 'AZURE',
+ INSTAGRAM = 'INSTAGRAM',
+ KEYCLOAK = 'KEYCLOAK',
+}
diff --git a/services/authentication-service/src/modules/auth/models/idp-configuration.dto.ts b/services/authentication-service/src/modules/auth/models/idp-configuration.dto.ts
new file mode 100644
index 0000000000..0d5ed29f1d
--- /dev/null
+++ b/services/authentication-service/src/modules/auth/models/idp-configuration.dto.ts
@@ -0,0 +1,83 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import {model, property} from '@loopback/repository';
+import {CoreModel} from '@sourceloop/core';
+import {ModelPropertyDescriptionString} from './model-property-description.enum';
+
+@model({
+ description: 'This is signature for idp configuration.',
+})
+export class IdpConfiguration extends CoreModel {
+ @property({
+ type: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ authorization_endpoint: string;
+
+ @property({
+ type: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ end_session_endpoint: string;
+
+ @property({
+ type: 'array',
+ itemType: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ id_token_signing_alg_values_supported: string[];
+
+ @property({
+ type: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ issuer: string;
+
+ @property({
+ type: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ jwks_uri: string;
+
+ @property({
+ type: 'array',
+ itemType: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ response_types_supported: string[];
+
+ @property({
+ type: 'array',
+ itemType: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ scopes_supported: string[];
+
+ @property({
+ type: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ token_endpoint: string;
+
+ @property({
+ type: 'array',
+ itemType: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ token_endpoint_auth_methods_supported: string[];
+
+ @property({
+ type: 'string',
+ description: ModelPropertyDescriptionString.reqStrPropDesc,
+ required: true,
+ })
+ userinfo_endpoint: string;
+}
diff --git a/services/authentication-service/src/modules/auth/models/index.ts b/services/authentication-service/src/modules/auth/models/index.ts
index 6b2838d7f3..ba78819a36 100644
--- a/services/authentication-service/src/modules/auth/models/index.ts
+++ b/services/authentication-service/src/modules/auth/models/index.ts
@@ -2,6 +2,9 @@ export * from './auth-refresh-token-request.dto';
export * from './auth-token-request.dto';
export * from './auth-user.model';
export * from './client-auth-request.dto';
+export * from './idp-auth-request.dto';
+export * from './idp-auth.method';
+export * from './idp-configuration.dto';
export * from './login-request.dto';
export * from './model-property-description.enum';
export * from './otp-login-request.dto';
diff --git a/services/authentication-service/src/services/idp-login.service.ts b/services/authentication-service/src/services/idp-login.service.ts
new file mode 100644
index 0000000000..ea40a5af5b
--- /dev/null
+++ b/services/authentication-service/src/services/idp-login.service.ts
@@ -0,0 +1,487 @@
+import {BindingScope, inject, injectable} from '@loopback/core';
+import {AnyObject, repository} from '@loopback/repository';
+import {HttpErrors, RequestContext} from '@loopback/rest';
+import {AuthenticateErrorKeys, ILogger, LOGGER} from '@sourceloop/core';
+import crypto from 'crypto';
+import {
+ authenticate,
+ AuthErrorKeys,
+ ClientAuthCode,
+ STRATEGY,
+} from 'loopback4-authentication';
+import moment from 'moment';
+import {LoginType} from '../enums';
+import {AuthServiceBindings} from '../keys';
+import {AuthClient, LoginActivity, User, UserTenant} from '../models';
+import {
+ AuthTokenRequest,
+ AuthUser,
+ IdpConfiguration,
+ TokenResponse,
+} from '../modules/auth';
+import {
+ AuthCodeBindings,
+ CodeReaderFn,
+ JwtPayloadFn,
+ JWTSignerFn,
+ JWTVerifierFn,
+} from '../providers';
+import {
+ AuthClientRepository,
+ LoginActivityRepository,
+ RefreshTokenRepository,
+ RevokedTokenRepository,
+ UserRepository,
+ UserTenantRepository,
+} from '../repositories';
+import {ActorId, ExternalTokens, IUserActivity} from '../types';
+
+const clockSkew = 300;
+const nonceTime = 3600;
+const nonceCount = 10;
+
+@injectable({scope: BindingScope.TRANSIENT})
+export class IdpLoginService {
+ constructor(
+ @repository(AuthClientRepository)
+ public authClientRepository: AuthClientRepository,
+ @repository(UserRepository)
+ public userRepo: UserRepository,
+ @repository(UserTenantRepository)
+ public userTenantRepo: UserTenantRepository,
+ @repository(RefreshTokenRepository)
+ public refreshTokenRepo: RefreshTokenRepository,
+ @repository(RevokedTokenRepository)
+ public revokedTokensRepo: RevokedTokenRepository,
+ @inject(LOGGER.LOGGER_INJECT) public logger: ILogger,
+ @repository(LoginActivityRepository)
+ private readonly loginActivityRepo: LoginActivityRepository,
+ @inject(AuthServiceBindings.ActorIdKey)
+ private readonly actorKey: ActorId,
+ @inject.context() private readonly ctx: RequestContext,
+ @inject(AuthCodeBindings.CODEREADER_PROVIDER)
+ private readonly codeReader: CodeReaderFn,
+ @inject(AuthCodeBindings.JWT_VERIFIER, {optional: true})
+ private readonly jwtVerifier: JWTVerifierFn,
+ @inject(AuthCodeBindings.JWT_SIGNER)
+ private readonly jwtSigner: JWTSignerFn,
+ @inject(AuthServiceBindings.JWTPayloadProvider)
+ private readonly getJwtPayload: JwtPayloadFn,
+ @inject(AuthServiceBindings.MarkUserActivity, {optional: true})
+ private readonly userActivity?: IUserActivity,
+ ) { }
+
+ @authenticate(STRATEGY.COGNITO_OAUTH2, {
+ callbackURL: process.env.COGNITO_AUTH_CALLBACK_URL,
+ clientDomain: process.env.COGNITO_AUTH_CLIENT_DOMAIN,
+ clientID: process.env.COGNITO_AUTH_CLIENT_ID,
+ clientSecret: process.env.COGNITO_AUTH_CLIENT_SECRET,
+ region: process.env.COGNITO_AUTH_REGION,
+ })
+ async loginViaCognito(): Promise {
+ // do nothing
+ }
+
+ @authenticate(STRATEGY.GOOGLE_OAUTH2, {
+ accessType: 'offline',
+ scope: ['profile', 'email'],
+ authorizationURL: process.env.GOOGLE_AUTH_URL,
+ callbackURL: process.env.GOOGLE_AUTH_CALLBACK_URL,
+ clientID: process.env.GOOGLE_AUTH_CLIENT_ID,
+ clientSecret: process.env.GOOGLE_AUTH_CLIENT_SECRET,
+ tokenURL: process.env.GOOGLE_AUTH_TOKEN_URL,
+ })
+ async loginViaGoogle(): Promise {
+ // do nothing
+ }
+
+ @authenticate(STRATEGY.SAML, {
+ accessType: 'offline',
+ scope: ['profile', 'email'],
+ callbackURL: process.env.SAML_CALLBACK_URL,
+ issuer: process.env.SAML_ISSUER,
+ cert: process.env.SAML_CERT,
+ entryPoint: process.env.SAML_ENTRY_POINT,
+ audience: process.env.SAML_AUDIENCE,
+ logoutUrl: process.env.SAML_LOGOUT_URL,
+ passReqToCallback: !!+(process.env.SAML_AUTH_PASS_REQ_CALLBACK ?? 0),
+ validateInResponseTo: !!+(process.env.VALIDATE_RESPONSE ?? 1),
+ idpIssuer: process.env.IDP_ISSUER,
+ logoutCallbackUrl: process.env.SAML_LOGOUT_CALLBACK_URL,
+ })
+ async loginViaSaml(): Promise {
+ // do nothing
+ }
+
+ @authenticate(STRATEGY.FACEBOOK_OAUTH2, {
+ accessType: 'offline',
+ authorizationURL: process.env.FACEBOOK_AUTH_URL,
+ callbackURL: process.env.FACEBOOK_AUTH_CALLBACK_URL,
+ clientID: process.env.FACEBOOK_AUTH_CLIENT_ID,
+ clientSecret: process.env.FACEBOOK_AUTH_CLIENT_SECRET,
+ tokenURL: process.env.FACEBOOK_AUTH_TOKEN_URL,
+ })
+ async loginViaFacebook(): Promise {
+ // do nothing
+ }
+
+ @authenticate(STRATEGY.APPLE_OAUTH2, {
+ accessType: 'offline',
+ scope: ['name', 'email'],
+ callbackURL: process.env.APPLE_AUTH_CALLBACK_URL,
+ clientID: process.env.APPLE_AUTH_CLIENT_ID,
+ teamID: process.env.APPLE_AUTH_TEAM_ID,
+ keyID: process.env.APPLE_AUTH_KEY_ID,
+ privateKeyLocation: process.env.APPLE_AUTH_PRIVATE_KEY_LOCATION,
+ })
+ async loginViaApple(): Promise {
+ // do nothing
+ }
+
+ @authenticate(STRATEGY.AZURE_AD, {
+ scope: ['profile', 'email', 'openid', 'offline_access'],
+ identityMetadata: process.env.AZURE_IDENTITY_METADATA,
+ clientID: process.env.AZURE_AUTH_CLIENT_ID,
+ responseType: 'code',
+ responseMode: 'query',
+ redirectUrl: process.env.AZURE_AUTH_REDIRECT_URL,
+ clientSecret: process.env.AZURE_AUTH_CLIENT_SECRET,
+ allowHttpForRedirectUrl: !!+(
+ process.env.AZURE_AUTH_ALLOW_HTTP_REDIRECT ?? 1
+ ),
+ passReqToCallback: !!+(process.env.AZURE_AUTH_PASS_REQ_CALLBACK ?? 0),
+ validateIssuer: !!+(process.env.AZURE_AUTH_VALIDATE_ISSUER ?? 1),
+ useCookieInsteadOfSession: !!+(
+ process.env.AZURE_AUTH_COOKIE_INSTEAD_SESSION ?? 1
+ ),
+ cookieEncryptionKeys: [
+ {
+ key: process.env.AZURE_AUTH_COOKIE_KEY,
+ iv: process.env.AZURE_AUTH_COOKIE_IV,
+ },
+ ],
+ isB2c: !!+(process.env.AZURE_AUTH_B2C_TENANT ?? 0),
+ clockSkew: +(process.env.AZURE_AUTH_CLOCK_SKEW ?? clockSkew),
+ loggingLevel: process.env.AZURE_AUTH_LOG_LEVEL,
+ loggingNoPII: !!+(process.env.AZURE_AUTH_LOG_PII ?? 1),
+ nonceLifetime: +(process.env.AZURE_AUTH_NONCE_TIME ?? nonceTime),
+ nonceMaxAmount: +(process.env.AZURE_AUTH_NONCE_COUNT ?? nonceCount),
+ issuer: process.env.AZURE_AUTH_ISSUER,
+ cookieSameSite: !!+(process.env.AZURE_AUTH_COOKIE_SAME_SITE ?? 0),
+ })
+ async loginViaAzure(): Promise {
+ // do nothing
+ }
+
+ @authenticate(STRATEGY.INSTAGRAM_OAUTH2, {
+ accessType: 'offline',
+ authorizationURL: process.env.INSTAGRAM_AUTH_URL,
+ callbackURL: process.env.INSTAGRAM_AUTH_CALLBACK_URL,
+ clientID: process.env.INSTAGRAM_AUTH_CLIENT_ID,
+ clientSecret: process.env.INSTAGRAM_AUTH_CLIENT_SECRET,
+ tokenURL: process.env.INSTAGRAM_AUTH_TOKEN_URL,
+ })
+ async loginViaInstagram(): Promise {
+ // do nothing
+ }
+
+ @authenticate(STRATEGY.KEYCLOAK, {
+ host: process.env.KEYCLOAK_HOST,
+ realm: process.env.KEYCLOAK_REALM,
+ clientID: process.env.KEYCLOAK_CLIENT_ID,
+ clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
+ callbackURL: process.env.KEYCLOAK_CALLBACK_URL,
+ authorizationURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`,
+ tokenURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
+ userInfoURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/userinfo`,
+ })
+ async loginViaKeycloak(): Promise {
+ // do nothing
+ }
+
+ /**
+ * The function `getOpenIdConfiguration` returns an IdpConfiguration object with specific properties
+ * set based on environment variables.
+ * @returns An IdpConfiguration object with the specified properties and values is being returned.
+ */
+ getOpenIdConfiguration() {
+ const config = new IdpConfiguration();
+ config.issuer = '';
+ config.authorization_endpoint = `${process.env.API_BASE_URL}/connect/auth`;
+ config.token_endpoint = `${process.env.API_BASE_URL}/connect/token`;
+ config.jwks_uri = '';
+ config.end_session_endpoint = `${process.env.API_BASE_URL}/connect/endsession`;
+ config.response_types_supported = ['code'];
+ config.scopes_supported = ['openid', 'email', 'phone', 'profile'];
+ config.id_token_signing_alg_values_supported = ['RS256'];
+ config.token_endpoint_auth_methods_supported = [
+ 'client_secret_basic',
+ 'client_secret_post',
+ ];
+ config.userinfo_endpoint = `${process.env.API_BASE_URL}/connect/userinfo`;
+ return config;
+ }
+
+ /**
+ * The function `generateToken` generates a JWT token for a client using a code
+ * and performs various authentication checks.
+ * @param {string} clientId - The `clientId` parameter in the `generateToken`
+ * function is a string that represents the client ID associated with the
+ * authentication request. It is used to identify the client making the request
+ * for generating a token.
+ * @param {string} code - The `code` parameter in the `generateToken` function is
+ * a string that represents the authorization code that will be used to generate
+ * a token for authentication and authorization purposes. This code is typically
+ * obtained during the authorization code flow in OAuth 2.0 when a user grants
+ * permission to a client application.
+ * @param {CodeReaderFn} codeReader - The `codeReader` parameter in the
+ * `generateToken` function is a function that reads a code and returns a result.
+ * It is injected using `@inject(AuthCodeBindings.CODEREADER_PROVIDER)` which
+ * means it is provided by a binding defined in the `AuthCodeBindings` namespace.
+ * The
+ * @returns The `generateToken` function is returning the result of calling
+ * `this.createJWT(payload, authClient, LoginType.ACCESS)` after performing
+ * various checks and operations.
+ */
+ public async generateToken(
+ request: AuthTokenRequest,
+ ): Promise {
+ const authClient = await this.authClientRepository.findOne({
+ where: {
+ clientId: request.clientId,
+ },
+ });
+ if (!authClient) {
+ throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
+ }
+ try {
+ const resultCode = await this.codeReader(request.code);
+ const payload = (await this.jwtVerifier(resultCode, {
+ audience: request.clientId,
+ })) as ClientAuthCode;
+ if (payload.mfa) {
+ throw new HttpErrors.Unauthorized(AuthErrorKeys.UserVerificationFailed);
+ }
+
+ if (
+ payload.userId &&
+ !(await this.userRepo.firstTimeUser(payload.userId))
+ ) {
+ await this.userRepo.updateLastLogin(payload.userId);
+ }
+
+ return await this.createJWT(payload, authClient, LoginType.ACCESS);
+ } catch (error) {
+ this.logger.error(error);
+ if (error.name === 'TokenExpiredError') {
+ throw new HttpErrors.Unauthorized(AuthErrorKeys.CodeExpired);
+ } else if (HttpErrors.HttpError.prototype.isPrototypeOf(error)) {
+ throw error;
+ } else {
+ throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
+ }
+ }
+ }
+
+ /**
+ * The `createJWT` function generates a JWT token for a user with specified
+ * payload and authentication client, handling user authentication and token
+ * expiration.
+ * @param payload - The `payload` parameter in the `createJWT` function is an
+ * object that contains information about the user and external tokens. It has
+ * the following properties:
+ * @param {AuthClient} authClient - The `authClient` parameter in the `createJWT`
+ * function represents the client that is requesting the JWT token generation. It
+ * contains information about the client, such as the client ID, access token
+ * expiration time, and refresh token expiration time. This information is used
+ * to customize the JWT token generation process based
+ * @param {LoginType} loginType - The `loginType` parameter in the `createJWT`
+ * function represents the type of login being performed, such as regular login,
+ * social login, or any other specific type of authentication method. It helps in
+ * determining the context of the login operation and can be used to customize
+ * the behavior or processing logic based
+ * @param {string} [tenantId] - The `tenantId` parameter in the `createJWT`
+ * function is an optional parameter of type string. It is used to specify the ID
+ * of the tenant for which the JWT token is being created. If provided, it is
+ * used in the process of generating the JWT payload. If not provided, the
+ * @returns A `TokenResponse` object is being returned, which contains the
+ * `accessToken`, `refreshToken`, and `expires` properties.
+ */
+ private async createJWT(
+ payload: ClientAuthCode & ExternalTokens,
+ authClient: AuthClient,
+ loginType: LoginType,
+ tenantId?: string,
+ ): Promise {
+ try {
+ const size = 32;
+ const ms = 1000;
+ let user: User | undefined;
+ if (payload.user) {
+ user = payload.user;
+ } else if (payload.userId) {
+ user = await this.userRepo.findById(payload.userId, {
+ include: [
+ {
+ relation: 'defaultTenant',
+ },
+ ],
+ });
+ if (payload.externalAuthToken && payload.externalRefreshToken) {
+ (user as AuthUser).externalAuthToken = payload.externalAuthToken;
+ (user as AuthUser).externalRefreshToken =
+ payload.externalRefreshToken;
+ }
+ } else {
+ // Do nothing and move ahead
+ }
+ if (!user) {
+ throw new HttpErrors.Unauthorized(
+ AuthenticateErrorKeys.UserDoesNotExist,
+ );
+ }
+ const data: AnyObject = await this.getJwtPayload(
+ user,
+ authClient,
+ tenantId,
+ );
+ const accessToken = await this.jwtSigner(data, {
+ expiresIn: authClient.accessTokenExpiration,
+ });
+ const refreshToken: string = crypto.randomBytes(size).toString('hex');
+ // Set refresh token into redis for later verification
+ await this.refreshTokenRepo.set(
+ refreshToken,
+ {
+ clientId: authClient.clientId,
+ userId: user.id,
+ username: user.username,
+ accessToken,
+ externalAuthToken: (user as AuthUser).externalAuthToken,
+ externalRefreshToken: (user as AuthUser).externalRefreshToken,
+ tenantId: data.tenantId,
+ },
+ {ttl: authClient.refreshTokenExpiration * ms},
+ );
+
+ const userTenant = await this.userTenantRepo.findOne({
+ where: {userId: user.id},
+ });
+ if (this.userActivity?.markUserActivity)
+ this.markUserActivity(user, userTenant, {...data}, loginType);
+
+ return new TokenResponse({
+ accessToken,
+ refreshToken,
+ expires: moment()
+ .add(authClient.accessTokenExpiration, 's')
+ .toDate()
+ .getTime(),
+ });
+ } catch (error) {
+ this.logger.error(error);
+ if (HttpErrors.HttpError.prototype.isPrototypeOf(error)) {
+ throw error;
+ } else {
+ throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
+ }
+ }
+ }
+
+ /**
+ * The function `markUserActivity` encrypts and stores user login activity,
+ * including IP address and payload, in a database.
+ * @param {User} user - The `user` parameter in the `markUserActivity` function
+ * represents the user who is performing the activity for which you are marking the
+ * login activity. This user object likely contains information about the user,
+ * such as their ID, name, email, etc. It is used to identify the actor of the
+ * @param {UserTenant | null} userTenant - The `userTenant` parameter in the
+ * `markUserActivity` function represents the tenant associated with the user. It
+ * can be either an object of type `UserTenant` or `null` if there is no specific
+ * tenant assigned to the user. The function uses this parameter to determine the
+ * actor and tenant
+ * @param {AnyObject} payload - The `payload` parameter in the `markUserActivity`
+ * function is the data that you want to encrypt and store as part of the user's
+ * login activity. In the provided code snippet, the `payload` is first converted
+ * to a JSON string using `JSON.stringify(payload)`. Then, it is
+ * @param {LoginType} loginType - The `loginType` parameter in the
+ * `markUserActivity` function represents the type of login activity being
+ * performed by the user. It is used to specify whether the user is logging in
+ * using a certain method or platform. Examples of `loginType` could include
+ * 'email', 'social', '2
+ */
+ public markUserActivity(
+ user: User,
+ userTenant: UserTenant | null,
+ payload: AnyObject,
+ loginType: LoginType,
+ ) {
+ const size = 16;
+ const encryptionKey = process.env.ENCRYPTION_KEY;
+
+ if (encryptionKey) {
+ const iv = crypto.randomBytes(size);
+
+ /* encryption of IP Address */
+ const cipherIp = crypto.createCipheriv('aes-256-gcm', encryptionKey, iv);
+ const ip =
+ this.ctx.request.headers['x-forwarded-for']?.toString() ??
+ this.ctx.request.socket.remoteAddress?.toString() ??
+ '';
+ const encyptIp = Buffer.concat([
+ cipherIp.update(ip, 'utf8'),
+ cipherIp.final(),
+ ]);
+ const authTagIp = cipherIp.getAuthTag();
+ const ipAddress = JSON.stringify({
+ iv: iv.toString('hex'),
+ encryptedData: encyptIp.toString('hex'),
+ authTag: authTagIp.toString('hex'),
+ });
+
+ /* encryption of Paylolad Address */
+ const cipherPayload = crypto.createCipheriv(
+ 'aes-256-gcm',
+ encryptionKey,
+ iv,
+ );
+ const activityPayload = JSON.stringify(payload);
+ const encyptPayload = Buffer.concat([
+ cipherPayload.update(activityPayload, 'utf8'),
+ cipherPayload.final(),
+ ]);
+ const authTagPayload = cipherIp.getAuthTag();
+ const tokenPayload = JSON.stringify({
+ iv: iv.toString('hex'),
+ encryptedData: encyptPayload.toString('hex'),
+ authTag: authTagPayload.toString('hex'),
+ });
+ // make an entry to mark the users login activity
+ let actor: string;
+ let tenantId: string;
+ if (userTenant) {
+ actor = userTenant[this.actorKey]?.toString() ?? '0';
+ tenantId = userTenant.tenantId;
+ } else {
+ actor = user['id']?.toString() ?? '0';
+ tenantId = user.defaultTenantId;
+ }
+ const loginActivity = new LoginActivity({
+ actor,
+ tenantId,
+ loginTime: new Date(),
+ tokenPayload,
+ loginType,
+ deviceInfo: this.ctx.request.headers['user-agent']?.toString(),
+ ipAddress,
+ });
+ this.loginActivityRepo.create(loginActivity).catch(() => {
+ this.logger.error(
+ `Failed to add the login activity => ${JSON.stringify(
+ loginActivity,
+ )}`,
+ );
+ });
+ }
+ }
+}
diff --git a/services/authentication-service/src/services/index.ts b/services/authentication-service/src/services/index.ts
index fc0953c0fc..0eabf4db74 100644
--- a/services/authentication-service/src/services/index.ts
+++ b/services/authentication-service/src/services/index.ts
@@ -3,10 +3,12 @@
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
import {ActiveUserFilterBuilderService} from './active-user-fliter-builder.service';
+import {IdpLoginService} from './idp-login.service';
import {LoginActivityHelperService} from './login-activity-helper.service';
import {LoginHelperService} from './login-helper.service';
import {OtpService} from './otp.service';
export * from './active-user-fliter-builder.service';
+export * from './idp-login.service';
export * from './login-activity-helper.service';
export * from './login-helper.service';
export * from './otp.service';
@@ -16,4 +18,5 @@ export const services = [
OtpService,
ActiveUserFilterBuilderService,
LoginActivityHelperService,
+ IdpLoginService,
];