diff --git a/package-lock.json b/package-lock.json index 8658e7f19a..d69be84149 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/.env.defaults b/services/authentication-service/.env.defaults index 26a0bdd8e9..3e85300184 100644 --- a/services/authentication-service/.env.defaults +++ b/services/authentication-service/.env.defaults @@ -61,4 +61,6 @@ AZURE_AUTH_COOKIE_KEY= #iv is 12 bit -AZURE_AUTH_COOKIE_IV= \ No newline at end of file +AZURE_AUTH_COOKIE_IV= + +MAX_JWT_KEYS=2 \ No newline at end of file diff --git a/services/authentication-service/.env.example b/services/authentication-service/.env.example index 3d351f2665..485162a606 100644 --- a/services/authentication-service/.env.example +++ b/services/authentication-service/.env.example @@ -83,3 +83,5 @@ AUTH0_DOMAIN= AUTH0_CLIENT_ID= AUTH0_CLIENT_SECRET= AUTH0_CALLBACK_URL= + +MAX_JWT_KEYS= diff --git a/services/authentication-service/migrations/mysql/migrations/20241105074844-add-jwt-keys-schema.js b/services/authentication-service/migrations/mysql/migrations/20241105074844-add-jwt-keys-schema.js new file mode 100644 index 0000000000..b1ebc0e60c --- /dev/null +++ b/services/authentication-service/migrations/mysql/migrations/20241105074844-add-jwt-keys-schema.js @@ -0,0 +1,59 @@ +'use strict'; + +var dbm; +var type; +var seed; +var fs = require('fs'); +var path = require('path'); +var Promise; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function (options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; + Promise = options.Promise; +}; + +exports.up = function (db) { + var filePath = path.join( + __dirname, + 'sqls', + '20241105074844-add-jwt-keys-schema-up.sql', + ); + return new Promise(function (resolve, reject) { + fs.readFile(filePath, {encoding: 'utf-8'}, function (err, data) { + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }).then(function (data) { + return db.runSql(data); + }); +}; + +exports.down = function (db) { + var filePath = path.join( + __dirname, + 'sqls', + '20241105074844-add-jwt-keys-schema-down.sql', + ); + return new Promise(function (resolve, reject) { + fs.readFile(filePath, {encoding: 'utf-8'}, function (err, data) { + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }).then(function (data) { + return db.runSql(data); + }); +}; + +exports._meta = { + version: 1, +}; diff --git a/services/authentication-service/migrations/mysql/migrations/sqls/20241105074844-add-jwt-keys-schema-down.sql b/services/authentication-service/migrations/mysql/migrations/sqls/20241105074844-add-jwt-keys-schema-down.sql new file mode 100644 index 0000000000..fbcccb9d0c --- /dev/null +++ b/services/authentication-service/migrations/mysql/migrations/sqls/20241105074844-add-jwt-keys-schema-down.sql @@ -0,0 +1 @@ +DROP TABLE main.jwt_keys; diff --git a/services/authentication-service/migrations/mysql/migrations/sqls/20241105074844-add-jwt-keys-schema-up.sql b/services/authentication-service/migrations/mysql/migrations/sqls/20241105074844-add-jwt-keys-schema-up.sql new file mode 100644 index 0000000000..7a3e493c39 --- /dev/null +++ b/services/authentication-service/migrations/mysql/migrations/sqls/20241105074844-add-jwt-keys-schema-up.sql @@ -0,0 +1,7 @@ +CREATE TABLE main.jwt_keys ( + id INT AUTO_INCREMENT PRIMARY KEY, + key_id VARCHAR(100) UNIQUE NOT NULL, + public_key TEXT NOT NULL, -- Public key in PEM format + private_key TEXT NOT NULL, -- Private key in PEM format + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/services/authentication-service/migrations/pg/migrations/20241105074844-add-jwt-keys-schema.js b/services/authentication-service/migrations/pg/migrations/20241105074844-add-jwt-keys-schema.js new file mode 100644 index 0000000000..b1ebc0e60c --- /dev/null +++ b/services/authentication-service/migrations/pg/migrations/20241105074844-add-jwt-keys-schema.js @@ -0,0 +1,59 @@ +'use strict'; + +var dbm; +var type; +var seed; +var fs = require('fs'); +var path = require('path'); +var Promise; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function (options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; + Promise = options.Promise; +}; + +exports.up = function (db) { + var filePath = path.join( + __dirname, + 'sqls', + '20241105074844-add-jwt-keys-schema-up.sql', + ); + return new Promise(function (resolve, reject) { + fs.readFile(filePath, {encoding: 'utf-8'}, function (err, data) { + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }).then(function (data) { + return db.runSql(data); + }); +}; + +exports.down = function (db) { + var filePath = path.join( + __dirname, + 'sqls', + '20241105074844-add-jwt-keys-schema-down.sql', + ); + return new Promise(function (resolve, reject) { + fs.readFile(filePath, {encoding: 'utf-8'}, function (err, data) { + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }).then(function (data) { + return db.runSql(data); + }); +}; + +exports._meta = { + version: 1, +}; diff --git a/services/authentication-service/migrations/pg/migrations/sqls/20241105074844-add-jwt-keys-schema-down.sql b/services/authentication-service/migrations/pg/migrations/sqls/20241105074844-add-jwt-keys-schema-down.sql new file mode 100644 index 0000000000..fbcccb9d0c --- /dev/null +++ b/services/authentication-service/migrations/pg/migrations/sqls/20241105074844-add-jwt-keys-schema-down.sql @@ -0,0 +1 @@ +DROP TABLE main.jwt_keys; diff --git a/services/authentication-service/migrations/pg/migrations/sqls/20241105074844-add-jwt-keys-schema-up.sql b/services/authentication-service/migrations/pg/migrations/sqls/20241105074844-add-jwt-keys-schema-up.sql new file mode 100644 index 0000000000..ba3615437c --- /dev/null +++ b/services/authentication-service/migrations/pg/migrations/sqls/20241105074844-add-jwt-keys-schema-up.sql @@ -0,0 +1,7 @@ +CREATE TABLE main.jwt_keys ( + id SERIAL PRIMARY KEY, + key_id VARCHAR(100) UNIQUE NOT NULL, + public_key TEXT NOT NULL, -- Public key in PEM format + private_key TEXT NOT NULL, -- Private key in PEM format + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/services/authentication-service/openapi.json b/services/authentication-service/openapi.json index 563a06a49d..9c15beb84a 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,200 @@ "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/AuthRefreshTokenRequestPartial" + } + } + }, + "x-parameter-index": 1 + }, + "operationId": "IdentityServerController.logout" + } + }, + "/connect/get-keys": { + "post": { + "x-controller-name": "IdentityServerController", + "x-operation-name": "getKeys", + "tags": [ + "IdentityServerController" + ], + "description": "Get the public keys", + "responses": { + "200": { + "description": "JWKS Keys" + }, + "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.getKeys" + } + }, + "/connect/rotate-keys": { + "post": { + "x-controller-name": "IdentityServerController", + "x-operation-name": "generateKeys", + "tags": [ + "IdentityServerController" + ], + "description": "Generate the set of public and private keys", + "responses": { + "200": { + "description": "JWKS Keys" + }, + "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.generateKeys" + } + }, + "/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", @@ -2671,6 +2865,21 @@ ], "additionalProperties": false }, + "AuthRefreshTokenRequestPartial": { + "title": "AuthRefreshTokenRequestPartial", + "type": "object", + "description": "(tsType: Partial, schemaOptions: { partial: true })", + "properties": { + "refreshToken": { + "type": "string" + }, + "tenantId": { + "type": "string" + } + }, + "additionalProperties": false, + "x-typescript-type": "Partial" + }, "loopback.Count": { "type": "object", "title": "loopback.Count", diff --git a/services/authentication-service/openapi.md b/services/authentication-service/openapi.md index afdb350d34..3a59b53cb7 100644 --- a/services/authentication-service/openapi.md +++ b/services/authentication-service/openapi.md @@ -201,6 +201,389 @@ auth_method: string This operation does not require authentication +## IdentityServerController.logout + + + +> Code samples + +```javascript +const inputBody = '{ + "refreshToken": "string", + "tenantId": "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", + "tenantId": "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", + "tenantId": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|Authorization|header|string|false|This is the access token which is required to authenticate user.| +|body|body|[AuthRefreshTokenRequestPartial](#schemaauthrefreshtokenrequestpartial)|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| + + + +## IdentityServerController.getKeys + + + +> Code samples + +```javascript + +fetch('/connect/get-keys', +{ + method: 'POST' + +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```javascript--nodejs +const fetch = require('node-fetch'); + +fetch('/connect/get-keys', +{ + method: 'POST' + +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +`POST /connect/get-keys` + +Get the public keys + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|JWKS Keys|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| + + + +## IdentityServerController.generateKeys + + + +> Code samples + +```javascript + +fetch('/connect/rotate-keys', +{ + method: 'POST' + +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```javascript--nodejs +const fetch = require('node-fetch'); + +fetch('/connect/rotate-keys', +{ + method: 'POST' + +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +`POST /connect/rotate-keys` + +Generate the set of public and private keys + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|JWKS Keys|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| + + + +## 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| + + + +## 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

+ + +

LoginActivityController

## LoginActivityController.getActiveUsers @@ -3114,7 +3497,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)|