From 3b16762f0ef3ced0dfb65c69dcb12f9a91525019 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sat, 8 Jun 2024 17:09:27 +0300 Subject: [PATCH] client confidentiality toggle --- migrations/0004_quiet_wolfsbane.sql | 1 + migrations/meta/0004_snapshot.json | 1072 +++++++++++++++++ migrations/meta/_journal.json | 7 + .../components/admin/AdminClientCard.svelte | 3 + src/lib/components/form/FormControl.svelte | 16 +- src/lib/i18n/en/admin.json | 7 +- src/lib/server/drizzle/schema.ts | 1 + .../server/oauth2/controller/authorization.ts | 6 + .../oauth2/controller/device-authorization.ts | 10 +- src/lib/server/oauth2/controller/token.ts | 4 +- .../controller/tokens/authorizationCode.ts | 40 +- .../server/oauth2/controller/tokens/device.ts | 13 + src/lib/server/oauth2/model/client.ts | 13 +- .../ssoadmin/oauth2/[uuid]/+page.server.ts | 14 +- .../ssoadmin/oauth2/[uuid]/+page.svelte | 13 +- .../ssoadmin/oauth2/new/+page.server.ts | 13 +- src/routes/ssoadmin/oauth2/new/+page.svelte | 49 +- 17 files changed, 1218 insertions(+), 64 deletions(-) create mode 100644 migrations/0004_quiet_wolfsbane.sql create mode 100644 migrations/meta/0004_snapshot.json diff --git a/migrations/0004_quiet_wolfsbane.sql b/migrations/0004_quiet_wolfsbane.sql new file mode 100644 index 0000000..f84cede --- /dev/null +++ b/migrations/0004_quiet_wolfsbane.sql @@ -0,0 +1 @@ +ALTER TABLE `o_auth2_client` ADD `confidential` tinyint DEFAULT 1 NOT NULL; \ No newline at end of file diff --git a/migrations/meta/0004_snapshot.json b/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..ee48990 --- /dev/null +++ b/migrations/meta/0004_snapshot.json @@ -0,0 +1,1072 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "7567b993-bf87-45c6-a2a6-7c5a30fc891e", + "prevId": "3085d3b2-9695-421a-ba90-55681e0a812a", + "tables": { + "audit_log": { + "name": "audit_log", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_ip": { + "name": "actor_ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_ua": { + "name": "actor_ua", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "flagged": { + "name": "flagged", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "actorId": { + "name": "actorId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "audit_log_actorId_user_id_fk": { + "name": "audit_log_actorId_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "actorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "document": { + "name": "document", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorId": { + "name": "authorId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "document_authorId_user_id_fk": { + "name": "document_authorId_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": [ + "authorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "o_auth2_client": { + "name": "o_auth2_client", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "client_id": { + "name": "client_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grants": { + "name": "grants", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('authorization_code')" + }, + "activated": { + "name": "activated", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "verified": { + "name": "verified", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "confidential": { + "name": "confidential", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "pictureId": { + "name": "pictureId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ownerId": { + "name": "ownerId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_pictureId_upload_id_fk": { + "name": "o_auth2_client_pictureId_upload_id_fk", + "tableFrom": "o_auth2_client", + "tableTo": "upload", + "columnsFrom": [ + "pictureId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "o_auth2_client_ownerId_user_id_fk": { + "name": "o_auth2_client_ownerId_user_id_fk", + "tableFrom": "o_auth2_client", + "tableTo": "user", + "columnsFrom": [ + "ownerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "IDX_e9d16c213910ad57bd05e97b42": { + "name": "IDX_e9d16c213910ad57bd05e97b42", + "columns": [ + "client_id" + ] + } + } + }, + "o_auth2_client_authorization": { + "name": "o_auth2_client_authorization", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "current": { + "name": "current", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_authorization_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_client_authorization_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_client_authorization", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_client_authorization_userId_user_id_fk": { + "name": "o_auth2_client_authorization_userId_user_id_fk", + "tableFrom": "o_auth2_client_authorization", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "o_auth2_client_manager": { + "name": "o_auth2_client_manager", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issuerId": { + "name": "issuerId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_manager_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_client_manager_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_client_manager", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_client_manager_userId_user_id_fk": { + "name": "o_auth2_client_manager_userId_user_id_fk", + "tableFrom": "o_auth2_client_manager", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_client_manager_issuerId_user_id_fk": { + "name": "o_auth2_client_manager_issuerId_user_id_fk", + "tableFrom": "o_auth2_client_manager", + "tableTo": "user", + "columnsFrom": [ + "issuerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "o_auth2_client_url": { + "name": "o_auth2_client_url", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('redirect_uri','terms','privacy','website')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_client_url_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_client_url_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_client_url", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "o_auth2_token": { + "name": "o_auth2_token", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "enum('code','device_code','access_token','refresh_token')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp()" + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "pcke": { + "name": "pcke", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "o_auth2_token_userId_user_id_fk": { + "name": "o_auth2_token_userId_user_id_fk", + "tableFrom": "o_auth2_token", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "o_auth2_token_clientId_o_auth2_client_id_fk": { + "name": "o_auth2_token_clientId_o_auth2_client_id_fk", + "tableFrom": "o_auth2_token", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "privilege": { + "name": "privilege", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "clientId": { + "name": "clientId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "automatic": { + "name": "automatic", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "privilege_clientId_o_auth2_client_id_fk": { + "name": "privilege_clientId_o_auth2_client_id_fk", + "tableFrom": "privilege", + "tableTo": "o_auth2_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "upload": { + "name": "upload", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "original_name": { + "name": "original_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mimetype": { + "name": "mimetype", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file": { + "name": "file", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uploaderId": { + "name": "uploaderId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "upload_uploaderId_user_id_fk": { + "name": "upload_uploaderId_user_id_fk", + "tableFrom": "upload", + "tableTo": "user", + "columnsFrom": [ + "uploaderId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "uuid": { + "name": "uuid", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "varchar(26)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "activated": { + "name": "activated", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "activity_at": { + "name": "activity_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp()" + }, + "pictureId": { + "name": "pictureId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_pictureId_upload_id_fk": { + "name": "user_pictureId_upload_id_fk", + "tableFrom": "user", + "tableTo": "upload", + "columnsFrom": [ + "pictureId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "IDX_a95e949168be7b7ece1a2382fe": { + "name": "IDX_a95e949168be7b7ece1a2382fe", + "columns": [ + "uuid" + ] + }, + "IDX_78a916df40e02a9deb1c4b75ed": { + "name": "IDX_78a916df40e02a9deb1c4b75ed", + "columns": [ + "username" + ] + }, + "IDX_e12875dfb3b1d92d7d7c5377e2": { + "name": "IDX_e12875dfb3b1d92d7d7c5377e2", + "columns": [ + "email" + ] + } + } + }, + "user_privileges_privilege": { + "name": "user_privileges_privilege", + "columns": { + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "privilegeId": { + "name": "privilegeId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "IDX_0664a7ff494a1859a09014c0f1": { + "name": "IDX_0664a7ff494a1859a09014c0f1", + "columns": [ + "userId" + ], + "isUnique": false + }, + "IDX_e71171f4ed20bc8564a1819d0b": { + "name": "IDX_e71171f4ed20bc8564a1819d0b", + "columns": [ + "privilegeId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_privileges_privilege_userId_user_id_fk": { + "name": "user_privileges_privilege_userId_user_id_fk", + "tableFrom": "user_privileges_privilege", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "user_privileges_privilege_privilegeId_privilege_id_fk": { + "name": "user_privileges_privilege_privilegeId_privilege_id_fk", + "tableFrom": "user_privileges_privilege", + "tableTo": "privilege", + "columnsFrom": [ + "privilegeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_token": { + "name": "user_token", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('generic','activation','deactivation','password','login','gdpr','totp','public_key','invite','recovery')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp(6)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_token_userId_user_id_fk": { + "name": "user_token_userId_user_id_fk", + "tableFrom": "user_token", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 7bb81d5..a7434a7 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1717843139528, "tag": "0003_round_killmonger", "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1717853591270, + "tag": "0004_quiet_wolfsbane", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/components/admin/AdminClientCard.svelte b/src/lib/components/admin/AdminClientCard.svelte index 6706252..ff22fe0 100644 --- a/src/lib/components/admin/AdminClientCard.svelte +++ b/src/lib/components/admin/AdminClientCard.svelte @@ -33,6 +33,9 @@
{$t('admin.oauth2.verified')}
{$t(`common.bool.${Boolean(client.verified)}`)}
+
{$t('admin.oauth2.confidential')}
+
{$t(`common.bool.${Boolean(client.confidential)}`)}
+
{$t('admin.oauth2.created')}
{formatDate(client.created_at)}
diff --git a/src/lib/components/form/FormControl.svelte b/src/lib/components/form/FormControl.svelte index 87dff8b..cc7bfcb 100644 --- a/src/lib/components/form/FormControl.svelte +++ b/src/lib/components/form/FormControl.svelte @@ -39,6 +39,10 @@ padding: 1rem; border-radius: 4px; } + + &[type='checkbox'] { + grid-area: box; + } } .form-control > :global(label) { @@ -66,12 +70,20 @@ } .form-control:has(input[type='checkbox']) { - flex-direction: row; - gap: 1rem; + display: grid; + grid-template-areas: 'box label' 'hint hint'; + grid-template-columns: min-content auto; + justify-content: start; + column-gap: 1rem; align-items: center; } + .form-control:has(input[type='checkbox']) > :global(span) { + grid-area: hint; + } + .form-control:has(input[type='checkbox']) > :global(label) { + grid-area: label; margin: 0; } diff --git a/src/lib/i18n/en/admin.json b/src/lib/i18n/en/admin.json index bc1cd16..71f1adc 100644 --- a/src/lib/i18n/en/admin.json +++ b/src/lib/i18n/en/admin.json @@ -43,7 +43,8 @@ "scopesHint": "The level of access to information you will be needing for this application.", "grants": "Available grant types", "grantsHint": "The OAuth2 authorization flows you will be using with this application.", - "grantsWarning": "Please note that id_token, implicit and device_code grant types are less secure than other flows, as they do not require a server to authenticate this application with its secret and there is potential for impersonation. You might want to make a separate application for using these flows and give them access to less information.", + "confidential": "Confidential", + "confidentialHint": "Uncheck this checkbox if you cannot reasonably guarantee the secrecy of your Client secret, such as in the case of your application being a native (desktop, mobile, etc.) application. Unchecking this will allow you to get tokens without client authentication. Some functionality may be limited in public applications.", "created": "Created at", "owner": "Created by", "ownerMe": "that's you!", @@ -97,8 +98,8 @@ "client_credentials": "Client credentials", "refresh_token": "Refresh token", "device_code": "Device authorization", - "implicit": "Implicit token", - "id_token": "ID token (OpenID Connect)" + "implicit": "Implicit access token (not recommended)", + "id_token": "Implicit ID token (not recommended)" }, "scopeTexts": { "picture": "Access profile picture URL", diff --git a/src/lib/server/drizzle/schema.ts b/src/lib/server/drizzle/schema.ts index ce20a34..e655381 100644 --- a/src/lib/server/drizzle/schema.ts +++ b/src/lib/server/drizzle/schema.ts @@ -52,6 +52,7 @@ export const oauth2Client = mysqlTable( grants: text('grants').default('authorization_code').notNull(), activated: tinyint('activated').default(0).notNull(), verified: tinyint('verified').default(0).notNull(), + confidential: tinyint('confidential').default(1).notNull(), pictureId: int('pictureId').references(() => upload.id, { onDelete: 'set null' }), ownerId: int('ownerId').references(() => user.id, { onDelete: 'set null' }), created_at: datetime('created_at', { mode: 'date', fsp: 6 }) diff --git a/src/lib/server/oauth2/controller/authorization.ts b/src/lib/server/oauth2/controller/authorization.ts index d2aaebe..5608dca 100644 --- a/src/lib/server/oauth2/controller/authorization.ts +++ b/src/lib/server/oauth2/controller/authorization.ts @@ -113,6 +113,12 @@ export class OAuth2AuthorizationController { throw new InvalidGrant('Invalid code challenge method'); } + if (client.confidential === 0 && !codeChallenge && grantTypes.includes('authorization_code')) { + throw new InvalidGrant( + 'A code_challenge is required for the authorization_code grant in non-confidential clients' + ); + } + return { client, user: locals.user, diff --git a/src/lib/server/oauth2/controller/device-authorization.ts b/src/lib/server/oauth2/controller/device-authorization.ts index 6c99377..28323ab 100644 --- a/src/lib/server/oauth2/controller/device-authorization.ts +++ b/src/lib/server/oauth2/controller/device-authorization.ts @@ -11,8 +11,8 @@ export class OAuth2DeviceAuthorizationController { let clientId: string | null = null; let clientSecret: string | null = null; if (body.client_id) { - clientId = body.client_id; - clientSecret = body.client_secret; + clientId = body.client_id as string; + clientSecret = body.client_secret as string; } else { if (!request.headers?.has('authorization')) { throw new InvalidRequest('No authorization header passed'); @@ -46,8 +46,10 @@ export class OAuth2DeviceAuthorizationController { throw new InvalidClient('Client not found'); } - // This flow is the only one we allow to use public access (secret not required) - if (clientSecret && !OAuth2Clients.checkSecret(client, clientSecret)) { + if ( + (client.confidential === 1 || clientSecret) && + !OAuth2Clients.checkSecret(client, clientSecret) + ) { throw new UnauthorizedClient('Invalid client secret'); } diff --git a/src/lib/server/oauth2/controller/token.ts b/src/lib/server/oauth2/controller/token.ts index f76318e..b2bf544 100644 --- a/src/lib/server/oauth2/controller/token.ts +++ b/src/lib/server/oauth2/controller/token.ts @@ -69,8 +69,8 @@ export class OAuth2TokenController { throw new InvalidClient('Client not found'); } - // Device code flow does not require client authentication - if (grantType !== 'device_code' || clientSecret) { + // client_credentials cannot be fetched in public clients. + if (client.confidential === 1 || clientSecret || grantType === 'client_credentials') { const valid = OAuth2Clients.checkSecret(client, clientSecret); if (!valid) { throw new UnauthorizedClient('Invalid client secret'); diff --git a/src/lib/server/oauth2/controller/tokens/authorizationCode.ts b/src/lib/server/oauth2/controller/tokens/authorizationCode.ts index 5121d70..6e96e23 100644 --- a/src/lib/server/oauth2/controller/tokens/authorizationCode.ts +++ b/src/lib/server/oauth2/controller/tokens/authorizationCode.ts @@ -61,32 +61,30 @@ export async function authorizationCode( const userId = code.userId as number; const clientId = client.client_id; - if (OAuth2Codes.getCodeChallenge) { - const { challenge, method } = OAuth2Codes.getCodeChallenge(code); + const { challenge, method } = OAuth2Codes.getCodeChallenge(code); - if (challenge && method) { - if (!codeVerifier) { - throw new InvalidGrant('Code verifier is required!'); - } - - if (method === 'plain' && !CryptoUtils.safeCompare(challenge, codeVerifier)) { - throw new InvalidGrant('Invalid code verifier!'); - } - - if ( - method === 'S256' && - !CryptoUtils.safeCompare( - CryptoUtils.createS256(codeVerifier), - CryptoUtils.sanitizeS256(challenge) - ) - ) { - throw new InvalidGrant('Invalid code verifier!'); - } + if (challenge && method) { + if (!codeVerifier) { + throw new InvalidGrant('Code verifier is required!'); } - // console.debug('Code passed PCKE check'); + if (method === 'plain' && !CryptoUtils.safeCompare(challenge, codeVerifier)) { + throw new InvalidGrant('Invalid code verifier!'); + } + + if ( + method === 'S256' && + !CryptoUtils.safeCompare( + CryptoUtils.createS256(codeVerifier), + CryptoUtils.sanitizeS256(challenge) + ) + ) { + throw new InvalidGrant('Invalid code verifier!'); + } } + // console.debug('Code passed PCKE check'); + if (!OAuth2Clients.checkGrantType(client, 'refresh_token')) { // console.debug('Client does not allow grant type refresh_token, skip creation'); } else { diff --git a/src/lib/server/oauth2/controller/tokens/device.ts b/src/lib/server/oauth2/controller/tokens/device.ts index 9e2bf0e..dfe9e8f 100644 --- a/src/lib/server/oauth2/controller/tokens/device.ts +++ b/src/lib/server/oauth2/controller/tokens/device.ts @@ -5,6 +5,7 @@ import { OAuth2AccessTokens, OAuth2Clients, OAuth2DeviceCodes, + OAuth2RefreshTokens, OAuth2Tokens, OAuth2Users } from '../../model'; @@ -55,6 +56,18 @@ export async function device(client: OAuth2Client, deviceCode: string) { } } + if (OAuth2Clients.checkGrantType(client, 'refresh_token')) { + try { + resObj.refresh_token = await OAuth2RefreshTokens.create( + user.id, + client.client_id, + cleanScope + ); + } catch (err) { + throw new ServerError('Failed to call refreshTokens.create function'); + } + } + resObj.expires_in = OAuth2Tokens.tokenTtl; await OAuth2DeviceCodes.removeByCode(deviceCode); diff --git a/src/lib/server/oauth2/model/client.ts b/src/lib/server/oauth2/model/client.ts index 2d37ada..2a5f311 100644 --- a/src/lib/server/oauth2/model/client.ts +++ b/src/lib/server/oauth2/model/client.ts @@ -176,7 +176,7 @@ export class OAuth2Clients { return list; } - static checkSecret(client: OAuth2Client, secret: string) { + static checkSecret(client: OAuth2Client, secret: string | null) { return client.client_secret === secret; } @@ -316,7 +316,13 @@ export class OAuth2Clients { }; } - static async createClient(subject: User, title: string, redirect: string, description?: string) { + static async createClient( + subject: User, + title: string, + redirect: string, + description?: string, + confidential: boolean = true + ) { const uid = CryptoUtils.createUUID(); const secret = CryptoUtils.generateSecret(); @@ -330,7 +336,8 @@ export class OAuth2Clients { ownerId: subject.id, created_at: new Date(), activated: 1, - verified: 0 + verified: 0, + confidential: Number(confidential) }); await DB.drizzle.insert(oauth2ClientUrl).values({ diff --git a/src/routes/ssoadmin/oauth2/[uuid]/+page.server.ts b/src/routes/ssoadmin/oauth2/[uuid]/+page.server.ts index b366ed3..f378341 100644 --- a/src/routes/ssoadmin/oauth2/[uuid]/+page.server.ts +++ b/src/routes/ssoadmin/oauth2/[uuid]/+page.server.ts @@ -25,6 +25,7 @@ interface UpdateRequest { description: string; activated?: string; verified?: string; + confidential?: string; } interface AddPrivilegeRequest { @@ -80,16 +81,18 @@ export const actions = { const { details, fullPrivileges } = await getActionData(locals, uuid); const body = await request.formData(); - const { title, description, activated, verified } = Changesets.take( - ['title', 'description', 'activated', 'verified'], - body - ); + const { title, description, activated, verified, confidential } = + Changesets.take( + ['title', 'description', 'activated', 'verified', 'confidential'], + body + ); if (!!verified && !fullPrivileges) { return fail(403, { errors: ['forbidden'] }); } const actuallyVerified = fullPrivileges ? Number(!!verified) : undefined; + const actuallyConfidential = fullPrivileges ? Number(!!confidential) : undefined; const actuallyActivated = Number(!!activated); if (title && (title.length < 3 || title.length > 32)) { @@ -104,7 +107,8 @@ export const actions = { title, description, verified: actuallyVerified, - activated: actuallyActivated + activated: actuallyActivated, + confidential: actuallyConfidential }); return { errors: [] }; diff --git a/src/routes/ssoadmin/oauth2/[uuid]/+page.svelte b/src/routes/ssoadmin/oauth2/[uuid]/+page.svelte index f8a68fd..c40c23b 100644 --- a/src/routes/ssoadmin/oauth2/[uuid]/+page.svelte +++ b/src/routes/ssoadmin/oauth2/[uuid]/+page.svelte @@ -102,6 +102,18 @@ /> + + + + {$t('admin.oauth2.confidentialHint')} + + {$t('admin.oauth2.grants')}

{$t('admin.oauth2.grantsHint')}

-

{$t('admin.oauth2.grantsWarning')}

diff --git a/src/routes/ssoadmin/oauth2/new/+page.server.ts b/src/routes/ssoadmin/oauth2/new/+page.server.ts index ada19cb..f96af15 100644 --- a/src/routes/ssoadmin/oauth2/new/+page.server.ts +++ b/src/routes/ssoadmin/oauth2/new/+page.server.ts @@ -7,6 +7,7 @@ interface CreateClientRequest { title: string; description: string; redirectUri: string; + confidential?: string; } export const actions = { @@ -16,8 +17,8 @@ export const actions = { ]); const body = await request.formData(); - const { title, description, redirectUri } = Changesets.take( - ['title', 'description', 'redirectUri'], + const { title, description, redirectUri, confidential } = Changesets.take( + ['title', 'description', 'redirectUri', 'confidential'], body ); @@ -33,7 +34,13 @@ export const actions = { return fail(400, { errors: ['noRedirect'] }); } - const uuid = await OAuth2Clients.createClient(currentUser, title, redirectUri, description); + const uuid = await OAuth2Clients.createClient( + currentUser, + title, + redirectUri, + description, + !!confidential + ); return redirect(303, `/ssoadmin/oauth2/${uuid}`); } diff --git a/src/routes/ssoadmin/oauth2/new/+page.svelte b/src/routes/ssoadmin/oauth2/new/+page.svelte index 192f892..44079dc 100644 --- a/src/routes/ssoadmin/oauth2/new/+page.svelte +++ b/src/routes/ssoadmin/oauth2/new/+page.svelte @@ -7,6 +7,7 @@ import { t } from '$lib/i18n'; import type { ActionData } from './$types'; import { env } from '$env/dynamic/public'; + import SplitView from '$lib/components/container/SplitView.svelte'; export let form: ActionData; @@ -17,27 +18,35 @@

{$t('admin.oauth2.new')}

- - - + + + + - - - - - + + + + + - - - - + + + + {$t('admin.oauth2.confidentialHint')} + - - -