diff --git a/package-lock.json b/package-lock.json
index e93951340a..3c1c211fe5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11322,6 +11322,16 @@
"@types/node": "*"
}
},
+ "node_modules/@types/node-jose": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@types/node-jose/-/node-jose-1.1.13.tgz",
+ "integrity": "sha512-QjMd4yhwy1EvSToQn0YI3cD29YhyfxFwj7NecuymjLys2/P0FwxWnkgBlFxCai6Y3aBCe7rbwmqwJJawxlgcXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/normalize-package-data": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
@@ -26948,6 +26958,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.2.0.tgz",
"integrity": "sha512-XPCvJRr94SjLrSIm4pbYHKLEaOsDvJCpyFw/6V/KK/IXmyZ6SFBzAUDO9HQf4DB/nTEFcRGH87mNciOP23kFjw==",
+ "license": "Apache-2.0",
"dependencies": {
"base64url": "^3.0.1",
"buffer": "^6.0.3",
@@ -43742,6 +43753,7 @@
"moment": "^2.29.3",
"moment-timezone": "^0.5.34",
"node-fetch": "^2.6.6",
+ "node-jose": "^2.2.0",
"otplib": "^12.0.1",
"passport-apple": "^2.0.1",
"passport-auth0": "^1.4.4",
@@ -43768,6 +43780,7 @@
"@types/node": "^18.11.9",
"@types/node-fetch": "^2.6.1",
"@types/node-forge": "^1.3.4",
+ "@types/node-jose": "^1.1.13",
"@types/passport-apple": "^1.1.1",
"@types/passport-auth0": "^1.0.9",
"@types/passport-azure-ad": "^4.3.1",
diff --git a/services/authentication-service/openapi.json b/services/authentication-service/openapi.json
index 563a06a49d..4480a5cb19 100644
--- a/services/authentication-service/openapi.json
+++ b/services/authentication-service/openapi.json
@@ -752,7 +752,7 @@
],
"responses": {
"200": {
- "description": "Google Token Response,\n (Deprecated: Possible security issue if secret is passed via query params, \n please use the post endpoint)",
+ "description": "Google Token Response,\n (Deprecated: Possible security issue if secret is passed via query params,\n please use the post endpoint)",
"content": {
"application/json": {
"schema": {
@@ -1720,6 +1720,144 @@
"operationId": "IdentityServerController.connectAuth"
}
},
+ "/connect/endsession": {
+ "post": {
+ "x-controller-name": "IdentityServerController",
+ "x-operation-name": "logout",
+ "tags": [
+ "IdentityServerController"
+ ],
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "description": "To logout",
+ "responses": {
+ "200": {
+ "description": "Success Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SuccessResponse"
+ }
+ }
+ }
+ },
+ "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"
+ }
+ },
+ "parameters": [
+ {
+ "name": "Authorization",
+ "in": "header",
+ "schema": {
+ "type": "string"
+ },
+ "description": "This is the access token which is required to authenticate user."
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RefreshTokenRequestPartial"
+ }
+ }
+ },
+ "x-parameter-index": 1
+ },
+ "operationId": "IdentityServerController.logout"
+ }
+ },
+ "/connect/token": {
+ "post": {
+ "x-controller-name": "IdentityServerController",
+ "x-operation-name": "getToken",
+ "tags": [
+ "IdentityServerController"
+ ],
+ "description": "Send the code received from the POST /auth/login api and get refresh token and access token (webapps)",
+ "responses": {
+ "200": {
+ "description": "Token Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/TokenResponse"
+ }
+ }
+ }
+ },
+ "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"
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AuthTokenRequest"
+ }
+ }
+ }
+ },
+ "operationId": "IdentityServerController.getToken"
+ }
+ },
+ "/connect/userinfo": {
+ "get": {
+ "x-controller-name": "IdentityServerController",
+ "x-operation-name": "me",
+ "tags": [
+ "IdentityServerController"
+ ],
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "description": "To get the user details",
+ "responses": {
+ "200": {
+ "description": "User Object",
+ "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.me"
+ }
+ },
"/google/logout": {
"post": {
"x-controller-name": "LogoutController",
diff --git a/services/authentication-service/openapi.md b/services/authentication-service/openapi.md
index afdb350d34..33c1bf613f 100644
--- a/services/authentication-service/openapi.md
+++ b/services/authentication-service/openapi.md
@@ -201,6 +201,276 @@ auth_method: string
This operation does not require authentication
+## IdentityServerController.logout
+
+
+
+> Code samples
+
+```javascript
+const inputBody = '{
+ "refreshToken": "string"
+}';
+const headers = {
+ 'Content-Type':'application/json',
+ 'Accept':'application/json',
+ 'Authorization':'string'
+};
+
+fetch('/connect/endsession',
+{
+ 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 = {
+ "refreshToken": "string"
+};
+const headers = {
+ 'Content-Type':'application/json',
+ 'Accept':'application/json',
+ 'Authorization':'string'
+};
+
+fetch('/connect/endsession',
+{
+ method: 'POST',
+ body: JSON.stringify(inputBody),
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+`POST /connect/endsession`
+
+To logout
+
+> Body parameter
+
+```json
+{
+ "refreshToken": "string"
+}
+```
+
+
Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|Authorization|header|string|false|This is the access token which is required to authenticate user.|
+|body|body|[RefreshTokenRequestPartial](#schemarefreshtokenrequestpartial)|false|none|
+
+> Example responses
+
+> 200 Response
+
+```json
+{
+ "success": true
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success Response|[SuccessResponse](#schemasuccessresponse)|
+|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|
+
+
+To perform this operation, you must be authenticated by means of one of the following methods:
+HTTPBearer
+
+
+## IdentityServerController.getToken
+
+
+
+> Code samples
+
+```javascript
+const inputBody = '{
+ "code": "string",
+ "clientId": "string"
+}';
+const headers = {
+ 'Content-Type':'application/json',
+ 'Accept':'application/json'
+};
+
+fetch('/connect/token',
+{
+ 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 = {
+ "code": "string",
+ "clientId": "string"
+};
+const headers = {
+ 'Content-Type':'application/json',
+ 'Accept':'application/json'
+};
+
+fetch('/connect/token',
+{
+ method: 'POST',
+ body: JSON.stringify(inputBody),
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+`POST /connect/token`
+
+Send the code received from the POST /auth/login api and get refresh token and access token (webapps)
+
+> Body parameter
+
+```json
+{
+ "code": "string",
+ "clientId": "string"
+}
+```
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|body|body|[AuthTokenRequest](#schemaauthtokenrequest)|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)|
+|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|
+
+
+This operation does not require authentication
+
+
+## IdentityServerController.me
+
+
+
+> Code samples
+
+```javascript
+
+const headers = {
+ 'Authorization':'Bearer {access-token}'
+};
+
+fetch('/connect/userinfo',
+{
+ 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('/connect/userinfo',
+{
+ method: 'GET',
+
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+`GET /connect/userinfo`
+
+To get the user details
+
+> Example responses
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|User Object|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
+
+
LoginActivityController
## LoginActivityController.getActiveUsers
@@ -3114,7 +3384,7 @@ fetch('/auth/google',
|Status|Meaning|Description|Schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|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)|[TokenResponse](#schematokenresponse)|
diff --git a/services/authentication-service/package.json b/services/authentication-service/package.json
index 1ded7b3b5f..6f8eb0a4c2 100644
--- a/services/authentication-service/package.json
+++ b/services/authentication-service/package.json
@@ -25,7 +25,6 @@
}
},
"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",
@@ -95,6 +94,7 @@
"moment": "^2.29.3",
"moment-timezone": "^0.5.34",
"node-fetch": "^2.6.6",
+ "node-jose": "^2.2.0",
"otplib": "^12.0.1",
"passport-apple": "^2.0.1",
"passport-auth0": "^1.4.4",
@@ -121,6 +121,7 @@
"@types/node": "^18.11.9",
"@types/node-fetch": "^2.6.1",
"@types/node-forge": "^1.3.4",
+ "@types/node-jose": "^1.1.13",
"@types/passport-apple": "^1.1.1",
"@types/passport-auth0": "^1.0.9",
"@types/passport-azure-ad": "^4.3.1",
diff --git a/services/authentication-service/src/component.ts b/services/authentication-service/src/component.ts
index 9647651bed..42c3ce92fc 100644
--- a/services/authentication-service/src/component.ts
+++ b/services/authentication-service/src/component.ts
@@ -81,6 +81,8 @@ import {
InstagramOauth2SignupProvider,
InstagramPostVerifyProvider,
InstagramPreVerifyProvider,
+ JwksJWTAsymmetricSignerProvider,
+ JwksJWTAsymmetricVerifierProvider,
JWTAsymmetricSignerProvider,
JWTAsymmetricVerifierProvider,
JwtPayloadProvider,
@@ -369,6 +371,14 @@ export class AuthenticationServiceComponent implements Component {
this.providers[AuthCodeBindings.JWT_VERIFIER.key] =
JWTSymmetricVerifierProvider;
}
+
+ if (this.authConfig?.useIdentityServer) {
+ this.providers[AuthCodeBindings.JWT_SIGNER.key] =
+ JwksJWTAsymmetricSignerProvider;
+ this.providers[AuthCodeBindings.JWT_VERIFIER.key] =
+ JwksJWTAsymmetricVerifierProvider;
+ }
+
this.providers[AuthServiceBindings.JWTPayloadProvider.key] =
JwtPayloadProvider;
this.providers[AuthServiceBindings.ForgotPasswordHandler.key] =
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 6601e7f00c..019494786c 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
@@ -53,7 +53,7 @@ export class AppleLoginController {
private readonly getAuthCode: AuthCodeGeneratorFn,
@inject('services.IdpLoginService')
private readonly idpLoginService: IdpLoginService,
- ) { }
+ ) {}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
@authorize({permissions: ['*']})
@@ -128,7 +128,8 @@ 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 8432586a99..5ce848569c 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
@@ -52,7 +52,7 @@ export class AzureLoginController {
private readonly getAuthCode: AuthCodeGeneratorFn,
@inject('services.IdpLoginService')
private readonly idpLoginService: IdpLoginService,
- ) { }
+ ) {}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
@authorize({permissions: ['*']})
@@ -215,7 +215,8 @@ 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 0293d210af..be92eaa359 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
@@ -54,7 +54,7 @@ export class CognitoLoginController {
private readonly getAuthCode: AuthCodeGeneratorFn,
@inject('services.IdpLoginService')
private readonly idpLoginService: IdpLoginService,
- ) { }
+ ) {}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
@authorize({permissions: ['*']})
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 305164a412..af619b63b0 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
@@ -53,7 +53,7 @@ export class FacebookLoginController {
private readonly getAuthCode: AuthCodeGeneratorFn,
@inject('services.IdpLoginService')
private readonly idpLoginService: IdpLoginService,
- ) { }
+ ) {}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
@authorize({permissions: ['*']})
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 cf61a609c0..195dd0feae 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
@@ -54,7 +54,7 @@ export class GoogleLoginController {
private readonly getAuthCode: AuthCodeGeneratorFn,
@inject('services.IdpLoginService')
private readonly idpLoginService: IdpLoginService,
- ) { }
+ ) {}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
@authorize({permissions: ['*']})
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
index 6c0c3102e2..32dc28d7f5 100644
--- a/services/authentication-service/src/modules/auth/controllers/identity-server.controller.ts
+++ b/services/authentication-service/src/modules/auth/controllers/identity-server.controller.ts
@@ -9,11 +9,9 @@ import {
requestBody,
} from '@loopback/rest';
import {
- AuthenticateErrorKeys,
CONTENT_TYPE,
ErrorCodes,
OPERATION_SECURITY_SPEC,
- RevokedTokenRepository,
STATUS_CODE,
SuccessResponse,
X_TS_TYPE,
@@ -27,11 +25,6 @@ import {
} from 'loopback4-authentication';
import {authorize} from 'loopback4-authorization';
import {
- AuthCodeBindings,
- AuthServiceBindings,
- CodeReaderFn,
- IUserActivity,
- LoginType,
RefreshTokenRepository,
RefreshTokenRequest,
UserRepository,
@@ -49,23 +42,17 @@ import {
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: ['*']})
@@ -134,8 +121,8 @@ export class IdentityServerController {
...ErrorCodes,
},
})
- async getConfig(): Promise {
- this.idpLoginService.getOpenIdConfiguration();
+ async getConfig(): Promise {
+ return this.idpLoginService.getOpenIdConfiguration();
}
@authorize({permissions: ['*']})
@@ -219,46 +206,20 @@ export class IdentityServerController {
})
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,
+ return this.idpLoginService.logoutUser(auth, req);
+ }
- key: refreshTokenModel.userId,
- });
+ @authorize({permissions: ['*']})
+ @post('/connect/generate-keys', {
+ description: 'Generate the set of public and private keys',
+ responses: {
+ [STATUS_CODE.OK]: {
+ description: 'JWKS Keys',
+ },
+ ...ErrorCodes,
+ },
+ })
+ async generateKeys(): Promise {
+ return this.idpLoginService.rotateKeys();
}
}
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 a879f3459f..06a82a5cac 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
@@ -53,7 +53,7 @@ export class InstagramLoginController {
private readonly getAuthCode: AuthCodeGeneratorFn,
@inject('services.IdpLoginService')
private readonly idpLoginService: IdpLoginService,
- ) { }
+ ) {}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
@authorize({permissions: ['*']})
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 f0a9755dd5..b7253470db 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
@@ -54,7 +54,7 @@ export class KeycloakLoginController {
private readonly getAuthCode: AuthCodeGeneratorFn,
@inject('services.IdpLoginService')
private readonly idpLoginService: IdpLoginService,
- ) { }
+ ) {}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
@authorize({permissions: ['*']})
diff --git a/services/authentication-service/src/modules/auth/controllers/logout.controller.ts b/services/authentication-service/src/modules/auth/controllers/logout.controller.ts
index 343932547a..b82373b3ca 100644
--- a/services/authentication-service/src/modules/auth/controllers/logout.controller.ts
+++ b/services/authentication-service/src/modules/auth/controllers/logout.controller.ts
@@ -26,28 +26,13 @@ import {
X_TS_TYPE,
} from '@sourceloop/core';
import {encode} from 'base-64';
-import crypto from 'crypto';
import {HttpsProxyAgent} from 'https-proxy-agent';
-import {
- authenticate,
- AuthenticationBindings,
- AuthErrorKeys,
- STRATEGY,
-} from 'loopback4-authentication';
+import {authenticate, AuthErrorKeys, STRATEGY} from 'loopback4-authentication';
import {authorize} from 'loopback4-authorization';
import fetch from 'node-fetch';
import {URLSearchParams} from 'url';
-import {LoginType} from '../../../enums';
import {AuthServiceBindings} from '../../../keys';
-import {
- AuthClient,
- LoginActivity,
- RefreshToken,
- RefreshTokenRequest,
- User,
- UserTenant,
-} from '../../../models';
-import {JwtPayloadFn} from '../../../providers';
+import {RefreshTokenRequest} from '../../../models';
import {
LoginActivityRepository,
RefreshTokenRepository,
@@ -55,7 +40,8 @@ import {
UserRepository,
UserTenantRepository,
} from '../../../repositories';
-import {ActorId, IUserActivity} from '../../../types';
+import {IdpLoginService} from '../../../services';
+import {ActorId} from '../../../types';
const proxyUrl = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY;
@@ -65,8 +51,6 @@ const getProxyAgent = () => {
}
return undefined;
};
-
-const size = 16;
const SUCCESS_RESPONSE = 'Success Response';
const AUTHENTICATE_USER =
'This is the access token which is required to authenticate user.';
@@ -87,12 +71,8 @@ export class LogoutController {
public userRepo: UserRepository,
@repository(UserTenantRepository)
public userTenantRepo: UserTenantRepository,
- @inject(AuthServiceBindings.JWTPayloadProvider)
- private readonly getJwtPayload: JwtPayloadFn,
- @inject(AuthenticationBindings.CURRENT_CLIENT)
- private readonly client: AuthClient | undefined,
- @inject(AuthServiceBindings.MarkUserActivity, {optional: true})
- private readonly userActivity?: IUserActivity,
+ @inject('services.IdpLoginService')
+ private readonly idpLoginService: IdpLoginService,
) {}
@authenticate(STRATEGY.BEARER, {
@@ -130,41 +110,7 @@ export class LogoutController {
})
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.markUserActivity(refreshTokenModel, user, userTenant);
- return new SuccessResponse({
- success: true,
-
- key: refreshTokenModel.userId,
- });
+ return this.idpLoginService.logoutUser(auth, req);
}
@authenticate(STRATEGY.BEARER, {
@@ -441,80 +387,4 @@ export class LogoutController {
key: refreshTokenModel.userId,
});
}
-
- private markUserActivity(
- refreshTokenModel: RefreshToken,
- user: User,
- userTenant: UserTenant | null,
- ) {
- 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({
- ...user,
- clientId: refreshTokenModel.clientId,
- });
- 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'),
- });
- let actor: string, tenantId;
- if (userTenant) {
- actor = userTenant[this.actorKey]?.toString() ?? '0';
- tenantId = userTenant.tenantId;
- } else {
- actor = user['id']?.toString() ?? '0';
- tenantId = user.defaultTenantId ?? '0';
- }
-
- const loginActivity = new LoginActivity({
- actor,
- tenantId,
- loginTime: new Date(),
- tokenPayload,
- deviceInfo: this.ctx.request.headers['user-agent']?.toString(),
- loginType: LoginType.LOGOUT,
- 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/modules/auth/controllers/saml-login.controller.ts b/services/authentication-service/src/modules/auth/controllers/saml-login.controller.ts
index dfc4a33cbf..7495a1bcdb 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
@@ -40,7 +40,7 @@ export class SamlLoginController {
private readonly getAuthCode: AuthCodeGeneratorFn,
@inject('services.IdpLoginService')
private readonly idpLoginService: IdpLoginService,
- ) { }
+ ) {}
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
@authorize({permissions: ['*']})
diff --git a/services/authentication-service/src/providers/index.ts b/services/authentication-service/src/providers/index.ts
index 01dca82f0e..eab3388eda 100644
--- a/services/authentication-service/src/providers/index.ts
+++ b/services/authentication-service/src/providers/index.ts
@@ -30,6 +30,8 @@ export * from './google-pre-verify.provider';
export * from './instagram-oauth2-signup.provider';
export * from './instagram-post-verify.provider';
export * from './instagram-pre-verify.provider';
+export * from './jwks-jwt-asymmertric-signer.provider';
+export * from './jwks-jwt-asymmetric-verifier.provider';
export * from './jwt-asymmetric-signer.provider';
export * from './jwt-asymmetric-verifier.provider';
export * from './jwt-payload.provider';
diff --git a/services/authentication-service/src/providers/jwks-jwt-asymmertric-signer.provider.ts b/services/authentication-service/src/providers/jwks-jwt-asymmertric-signer.provider.ts
new file mode 100644
index 0000000000..6baef6c953
--- /dev/null
+++ b/services/authentication-service/src/providers/jwks-jwt-asymmertric-signer.provider.ts
@@ -0,0 +1,39 @@
+// Copyright (c) 2023 Sourcefuse Technologies
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+import {Provider} from '@loopback/core';
+import * as fs from 'fs';
+import * as jwt from 'jsonwebtoken';
+import * as path from 'path';
+import {JWTSignerFn} from './types';
+
+const PRIVATE_KEYS_PATH = path.join(__dirname, 'private-keys.json');
+
+export class JwksJWTAsymmetricSignerProvider
+ implements Provider>
+{
+ value(): JWTSignerFn {
+ return async (data: T, options: jwt.SignOptions) => {
+ // Load private keys
+ const privateKeys = JSON.parse(
+ fs.readFileSync(PRIVATE_KEYS_PATH, 'utf8'),
+ );
+
+ // Use the latest private key (assuming it's the last one added)
+ const latestKeyId = Object.keys(privateKeys).pop();
+ const privateKey = privateKeys[latestKeyId ?? 0];
+
+ const accessToken = jwt.sign(
+ data,
+ {key: privateKey, passphrase: 'your-passphrase'},
+ {
+ ...options,
+ algorithm: 'RS256',
+ keyid: latestKeyId,
+ },
+ );
+ return accessToken;
+ };
+ }
+}
diff --git a/services/authentication-service/src/providers/jwks-jwt-asymmetric-verifier.provider.ts b/services/authentication-service/src/providers/jwks-jwt-asymmetric-verifier.provider.ts
new file mode 100644
index 0000000000..da423bbf08
--- /dev/null
+++ b/services/authentication-service/src/providers/jwks-jwt-asymmetric-verifier.provider.ts
@@ -0,0 +1,41 @@
+// Copyright (c) 2023 Sourcefuse Technologies
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+import {Provider} from '@loopback/core';
+import * as fs from 'fs';
+import * as jwt from 'jsonwebtoken';
+import * as jose from 'node-jose';
+import * as path from 'path';
+import {JWTVerifierFn} from './types';
+
+const JWKS_FILE_PATH = path.join(__dirname, 'jwks.json');
+
+export class JwksJWTAsymmetricVerifierProvider
+ implements Provider>
+{
+ value(): JWTVerifierFn {
+ return async (token: string, options: jwt.VerifyOptions) => {
+ const jwks = JSON.parse(fs.readFileSync(JWKS_FILE_PATH, 'utf8'));
+
+ // Get the key that matches the token's kid
+ const decoded = jwt.decode(token, {complete: true});
+ const kid = decoded?.header.kid;
+
+ // Find the public key by kid in the JWKS
+ const key = jwks.keys.find((k: {kid: string}) => k.kid === kid);
+
+ if (!key) {
+ throw new Error('Key not found for verification');
+ }
+
+ // Convert the JWK back to PEM format for verification
+ const pem = await jose.JWK.asKey(key).then(jwk => jwk.toPEM(false));
+
+ // Verify the token
+ const payload = jwt.verify(token, pem) as T;
+
+ return payload;
+ };
+ }
+}
diff --git a/services/authentication-service/src/services/idp-login.service.ts b/services/authentication-service/src/services/idp-login.service.ts
index ea40a5af5b..7b970b8a6c 100644
--- a/services/authentication-service/src/services/idp-login.service.ts
+++ b/services/authentication-service/src/services/idp-login.service.ts
@@ -1,8 +1,14 @@
-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 { BindingScope, inject, injectable } from '@loopback/core';
+import { AnyObject, repository } from '@loopback/repository';
+import { HttpErrors, RequestContext } from '@loopback/rest';
+import {
+ AuthenticateErrorKeys,
+ ILogger,
+ LOGGER,
+ SuccessResponse,
+} from '@sourceloop/core';
+import crypto, { generateKeyPairSync } from 'crypto';
+import * as fs from 'fs';
import {
authenticate,
AuthErrorKeys,
@@ -10,9 +16,17 @@ import {
STRATEGY,
} from 'loopback4-authentication';
import moment from 'moment';
-import {LoginType} from '../enums';
-import {AuthServiceBindings} from '../keys';
-import {AuthClient, LoginActivity, User, UserTenant} from '../models';
+import * as jose from 'node-jose';
+import * as path from 'path';
+import { LoginType } from '../enums';
+import { AuthServiceBindings } from '../keys';
+import {
+ AuthClient,
+ LoginActivity,
+ RefreshTokenRequest,
+ User,
+ UserTenant,
+} from '../models';
import {
AuthTokenRequest,
AuthUser,
@@ -20,12 +34,12 @@ import {
TokenResponse,
} from '../modules/auth';
import {
- AuthCodeBindings,
CodeReaderFn,
JwtPayloadFn,
JWTSignerFn,
JWTVerifierFn,
} from '../providers';
+import { AuthCodeBindings } from '../providers/keys';
import {
AuthClientRepository,
LoginActivityRepository,
@@ -34,13 +48,17 @@ import {
UserRepository,
UserTenantRepository,
} from '../repositories';
-import {ActorId, ExternalTokens, IUserActivity} from '../types';
+import { ActorId, ExternalTokens, IUserActivity } from '../types';
const clockSkew = 300;
const nonceTime = 3600;
const nonceCount = 10;
-@injectable({scope: BindingScope.TRANSIENT})
+// File path where JWKS and keys are stored
+const JWKS_FILE_PATH = path.join(__dirname, 'jwks.json');
+const PRIVATE_KEYS_PATH = path.join(__dirname, 'private-keys.json');
+
+@injectable({ scope: BindingScope.TRANSIENT })
export class IdpLoginService {
constructor(
@repository(AuthClientRepository)
@@ -61,13 +79,13 @@ export class IdpLoginService {
@inject.context() private readonly ctx: RequestContext,
@inject(AuthCodeBindings.CODEREADER_PROVIDER)
private readonly codeReader: CodeReaderFn,
- @inject(AuthCodeBindings.JWT_VERIFIER, {optional: true})
+ @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})
+ @inject(AuthServiceBindings.MarkUserActivity, { optional: true })
private readonly userActivity?: IUserActivity,
) { }
@@ -204,12 +222,14 @@ export class IdpLoginService {
* set based on environment variables.
* @returns An IdpConfiguration object with the specified properties and values is being returned.
*/
- getOpenIdConfiguration() {
+ async getOpenIdConfiguration() {
+ await this.generateJWKS();
+
const config = new IdpConfiguration();
- config.issuer = '';
+ config.issuer = `${process.env.API_BASE_URL}`;
config.authorization_endpoint = `${process.env.API_BASE_URL}/connect/auth`;
config.token_endpoint = `${process.env.API_BASE_URL}/connect/token`;
- config.jwks_uri = '';
+ config.jwks_uri = `${process.env.API_BASE_URL}/jwks.json`;
config.end_session_endpoint = `${process.env.API_BASE_URL}/connect/endsession`;
config.response_types_supported = ['code'];
config.scopes_supported = ['openid', 'email', 'phone', 'profile'];
@@ -361,14 +381,14 @@ export class IdpLoginService {
externalRefreshToken: (user as AuthUser).externalRefreshToken,
tenantId: data.tenantId,
},
- {ttl: authClient.refreshTokenExpiration * ms},
+ { ttl: authClient.refreshTokenExpiration * ms },
);
const userTenant = await this.userTenantRepo.findOne({
- where: {userId: user.id},
+ where: { userId: user.id },
});
if (this.userActivity?.markUserActivity)
- this.markUserActivity(user, userTenant, {...data}, loginType);
+ this.markUserActivity(user, userTenant, { ...data }, loginType);
return new TokenResponse({
accessToken,
@@ -484,4 +504,165 @@ export class IdpLoginService {
});
}
}
+
+ /**
+ * The `logoutUser` function in TypeScript handles the logout process for a user
+ * by revoking tokens and deleting refresh tokens.
+ * @param {string} auth - The `auth` parameter in the `logoutUser` function is a
+ * string that represents the authentication token. It is used to identify and
+ * authenticate the user who is attempting to log out. The function extracts the
+ * token from the `auth` parameter and performs various checks and operations
+ * related to user logout based on
+ * @param {RefreshTokenRequest} req - The `req` parameter in the `logoutUser`
+ * function is of type `RefreshTokenRequest`. It likely contains information
+ * related to the refresh token that is used to identify and authenticate the
+ * user during the logout process. This parameter may include properties such as
+ * `refreshToken`, which is essential for revoking
+ * @returns The `logoutUser` function returns a `Promise` that resolves to a
+ * `SuccessResponse` object with a `success` property set to `true` and a `key`
+ * property set to `refreshTokenModel.userId`.
+ */
+ async logoutUser(
+ auth: string,
+ 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.revokedTokensRepo.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.markUserActivity(
+ user,
+ userTenant,
+ {
+ ...user,
+ clientId: refreshTokenModel.clientId,
+ },
+ LoginType.LOGOUT,
+ );
+ return new SuccessResponse({
+ success: true,
+
+ key: refreshTokenModel.userId,
+ });
+ }
+
+ /**
+ * The function generates a JSON Web Key Set (JWKS) containing a RSA public key and saves it to a
+ * file.
+ */
+ async generateJWKS(): Promise {
+ const { publicKey } = generateKeyPairSync('rsa', {
+ modulusLength: 2048,
+ publicKeyEncoding: {
+ type: 'spki',
+ format: 'pem',
+ },
+ privateKeyEncoding: {
+ type: 'pkcs8',
+ format: 'pem',
+ cipher: 'aes-256-cbc',
+ passphrase: process.env.JWT_PRIVATE_KEY_PASSPHRASE,
+ },
+ });
+
+ const keyStore = jose.JWK.createKeyStore();
+ const key = await keyStore.add(publicKey, 'pem');
+
+ const jwks = {
+ keys: [key.toJSON()],
+ };
+
+ fs.writeFileSync(JWKS_FILE_PATH, JSON.stringify(jwks, null, 2));
+
+ console.log('JWKS has been generated and saved to jwks.json');
+ }
+
+ /**
+ * The function rotates the RSA keys used for signing JWT tokens. It generates a new key pair,
+ * adds the public key to the JWKS, and saves the private key to a file.
+ */
+ async rotateKeys() {
+ // Generate a new RSA key pair
+ const { publicKey, privateKey } = generateKeyPairSync('rsa', {
+ modulusLength: 2048,
+ publicKeyEncoding: {
+ type: 'spki',
+ format: 'pem',
+ },
+ privateKeyEncoding: {
+ type: 'pkcs8',
+ format: 'pem',
+ cipher: 'aes-256-cbc',
+ passphrase: process.env.JWT_PRIVATE_KEY_PASSPHRASE,
+ },
+ });
+
+ // Load the existing JWKS
+ let jwks;
+ try {
+ jwks = JSON.parse(fs.readFileSync(JWKS_FILE_PATH, 'utf8'));
+ } catch (err) {
+ jwks = { keys: [] };
+ }
+
+ // Create a new JWK KeyStore and add the new public key
+ const keyStore = jose.JWK.createKeyStore();
+ const newKey = await keyStore.add(publicKey, 'pem');
+
+ // Add a unique Key ID (kid) to the key
+ const newKid = `key-${Date.now()}`; // Can generate a better kid based on our needs
+ newKey.kid = newKid;
+
+ // Remove the oldest key if there are too many
+ if (jwks.keys.length >= 3) {
+ jwks.keys.shift();
+ }
+
+ // Add the new key to JWKS and save it
+ jwks.keys.push(newKey.toJSON());
+ fs.writeFileSync(JWKS_FILE_PATH, JSON.stringify(jwks, null, 2));
+
+ // Load existing private keys (if they exist)
+ let privateKeys;
+ try {
+ privateKeys = JSON.parse(fs.readFileSync(PRIVATE_KEYS_PATH, 'utf8'));
+ } catch (err) {
+ privateKeys = {};
+ }
+
+ // Keep only the new private key and remove old ones
+ if (Object.keys(privateKeys).length >= 3) {
+ const oldestKey = Object.keys(privateKeys)[0];
+ delete privateKeys[oldestKey];
+ }
+
+ // Save the new private key with the matching kid
+ privateKeys[newKid] = privateKey;
+ fs.writeFileSync(PRIVATE_KEYS_PATH, JSON.stringify(privateKeys, null, 2));
+
+ console.log('Keys have been rotated successfully.');
+ }
}
diff --git a/services/authentication-service/src/types.ts b/services/authentication-service/src/types.ts
index 2236374577..bd440315de 100644
--- a/services/authentication-service/src/types.ts
+++ b/services/authentication-service/src/types.ts
@@ -14,6 +14,7 @@ import {AuthRefreshTokenRequest} from './modules/auth';
// sonarignore:start
export interface IAuthServiceConfig extends IServiceConfig {
useSymmetricEncryption?: boolean;
+ useIdentityServer?: boolean;
}
// sonarignore:end