From 6591c0e9bc0b61714ad13f9d8047e56bc2d43673 Mon Sep 17 00:00:00 2001 From: Robert Goniszewski Date: Tue, 13 Feb 2024 17:52:33 +0100 Subject: [PATCH 1/3] feat: use token-based authentication for API requests Signed-off-by: Robert Goniszewski --- src/lib/pb.ts | 24 +++++++++-- src/routes/api/api-schema.json | 76 +++++++++++++++++++++++++++++++++- src/routes/api/auth/+server.ts | 56 +++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 src/routes/api/auth/+server.ts diff --git a/src/lib/pb.ts b/src/lib/pb.ts index 29b018d..c06a6b6 100644 --- a/src/lib/pb.ts +++ b/src/lib/pb.ts @@ -124,7 +124,24 @@ export async function authenticateUserApiRequest( pb: PocketBase, request: Request ): Promise { - const authKey = request.headers.get('Authorization') ?? ''; + const authKey = request.headers.get('Authorization')?.split(' ')[1]; + + if (!authKey) { + return { + owner: '', + disabled: null, + userRecord: null, + error: json( + { + success: false, + error: 'Unauthorized' + }, + { + status: 401 + } + ) + }; + } const response: authenticateUserApiRequestResponse = { owner: '', @@ -134,11 +151,10 @@ export async function authenticateUserApiRequest( }; try { - const [login, password] = atob(authKey.split(' ')[1]).split(':'); - + pb.authStore.save(authKey); const user = await pb .collection('users') - .authWithPassword(login, password) + .authRefresh() .then((user) => user.record); response.owner = user.id; diff --git a/src/routes/api/api-schema.json b/src/routes/api/api-schema.json index 3463bcc..2efdd25 100644 --- a/src/routes/api/api-schema.json +++ b/src/routes/api/api-schema.json @@ -3,7 +3,7 @@ "info": { "title": "Grimoire Integration API", "version": "0.2.3", - "description": "To authorize, use your user's credentials to create valid authorization header.\n\n Add `[USERNAME OR EMAIL]:[PASSWORD]` and [base64 encode it](https://www.base64encode.org/). Then use the resulting value as the `Authorization` header value, prefixed with `Bearer ` (separated by space). \n\nExample request with proper header (generated from `my-username:my-password`): \n\n```bash\ncurl --request GET \\ \n --url http://[GRIMOIRE_URL]/api/bookmarks \\ \n --header 'Authorization: Bearer bXktdXNlcm5hbWU6bXktcGFzc3dvcmQ='\n```" + "description": "To authorize, pass your user's credentials in a POST request to `/api/auth` to receive the token.\n\nThen use it as the `Authorization` header value, prefixed with `Bearer ` (separated by space). \n\nExample request with proper header: \n\n```bash\ncurl --request GET \\ \n --url http://[GRIMOIRE_URL]/api/bookmarks \\ \n --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'\n```" }, "servers": [ { @@ -27,6 +27,41 @@ } ], "paths": { + "/auth": { + "post": { + "summary": "Authenticate user", + "description": "Authenticate with your user credentials.", + "operationId": "authenticate", + "requestBody": { + "description": "User credentials", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "User's username or email" + }, + "password": { + "type": "string", + "description": "User's password" + } + } + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/AuthSuccess" + }, + "400": { + "$ref": "#/components/responses/AuthFailed" + } + } + } + }, "/bookmark/simple": { "post": { "summary": "Create a new quick bookmark", @@ -847,6 +882,45 @@ } }, "responses": { + "AuthFailed": { + "description": "Invalid credentials", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "examples": [false] + }, + "error": { + "type": "string" + } + } + } + } + } + }, + "AuthSuccess": { + "description": "User authenticated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "examples": [true] + }, + "token": { + "type": "string", + "description": "User token" + } + } + } + } + } + }, "Unauthorized": { "description": "Unauthorized", "content": { diff --git a/src/routes/api/auth/+server.ts b/src/routes/api/auth/+server.ts new file mode 100644 index 0000000..8b4f1b5 --- /dev/null +++ b/src/routes/api/auth/+server.ts @@ -0,0 +1,56 @@ +import joi from 'joi'; + +import { json } from '@sveltejs/kit'; + +import type { RequestHandler } from '@sveltejs/kit'; + +export const POST: RequestHandler = async ({ locals, request }) => { + const body = await request.json(); + + const schema = joi.object({ + login: joi.string().required(), + password: joi.string().required() + }); + + const { value, error } = schema.validate(body); + + if (error) { + return json( + { + success: false, + error: error?.message + }, + { + status: 401 + } + ); + } + + const { login, password } = value; + + try { + const authReponse = await locals.pb.collection('users').authWithPassword(login, password); + + const { token } = authReponse; + + return json( + { + success: true, + token + }, + { + status: 200 + } + ); + } catch (error: any) { + return json( + { + success: false, + error: error?.message + }, + { + status: 400 + } + ); + } +}; From 125b370f7a8ba3d3bdbc61775d894cc6f6531a68 Mon Sep 17 00:00:00 2001 From: Robert Goniszewski Date: Tue, 13 Feb 2024 18:02:06 +0100 Subject: [PATCH 2/3] chore: respond with better status codes Signed-off-by: Robert Goniszewski --- src/routes/api/api-schema.json | 3 +++ src/routes/api/auth/+server.ts | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/routes/api/api-schema.json b/src/routes/api/api-schema.json index 2efdd25..8b78971 100644 --- a/src/routes/api/api-schema.json +++ b/src/routes/api/api-schema.json @@ -58,6 +58,9 @@ }, "400": { "$ref": "#/components/responses/AuthFailed" + }, + "500": { + "$ref": "#/components/responses/ServerError" } } } diff --git a/src/routes/api/auth/+server.ts b/src/routes/api/auth/+server.ts index 8b4f1b5..5fbd4a7 100644 --- a/src/routes/api/auth/+server.ts +++ b/src/routes/api/auth/+server.ts @@ -21,7 +21,7 @@ export const POST: RequestHandler = async ({ locals, request }) => { error: error?.message }, { - status: 401 + status: 400 } ); } @@ -49,7 +49,7 @@ export const POST: RequestHandler = async ({ locals, request }) => { error: error?.message }, { - status: 400 + status: error?.status || 500 } ); } From 11aac557006f30d578ddd1d0bba4d5b92fde9903 Mon Sep 17 00:00:00 2001 From: Robert Goniszewski Date: Tue, 13 Feb 2024 18:06:25 +0100 Subject: [PATCH 3/3] fix: API schema: examples must be an array Signed-off-by: Robert Goniszewski --- src/routes/api/api-schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/api/api-schema.json b/src/routes/api/api-schema.json index 8b78971..d98e488 100644 --- a/src/routes/api/api-schema.json +++ b/src/routes/api/api-schema.json @@ -107,7 +107,7 @@ "required": false, "schema": { "type": "string", - "examples": "id1,id2,id3" + "examples": ["id1,id2,id3"] }, "description": "Comma separated bookmark IDs to retrieve" }