Skip to content

Commit a438f76

Browse files
committed
Restructure auth errors.
1 parent 1c3f03b commit a438f76

File tree

8 files changed

+89
-22
lines changed

8 files changed

+89
-22
lines changed

libs/lib-services/src/router/endpoint.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ export const executeEndpoint = async <I, O, C, P extends EndpointHandlerPayload<
1616
}
1717
const authorizer_response = await endpoint.authorize?.(payload);
1818
if (authorizer_response && !authorizer_response.authorized) {
19-
throw new errors.AuthorizationError(authorizer_response.errors);
19+
if (authorizer_response.error == null) {
20+
throw new errors.AuthorizationError2(errors.ErrorCode.PSYNC_S2101, 'Authorization failed');
21+
}
22+
throw authorizer_response.error;
2023
}
2124

2225
return endpoint.handler(payload);

libs/lib-services/src/router/router-definitions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ServiceError } from '@powersync/service-errors';
12
import { MicroValidator } from '../schema/definitions.js';
23

34
/**
@@ -22,7 +23,7 @@ export type AuthorizationResponse =
2223
}
2324
| {
2425
authorized: false;
25-
errors?: any[];
26+
error?: ServiceError | undefined;
2627
};
2728

2829
/**

packages/rsocket-router/src/router/ReactiveSocketRouter.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* to expose reactive websocket stream in an interface similar to
44
* other Journey micro routers.
55
*/
6-
import { errors, logger } from '@powersync/lib-services-framework';
6+
import { ErrorCode, errors, logger } from '@powersync/lib-services-framework';
77
import * as http from 'http';
88
import { Payload, RSocketServer } from 'rsocket-core';
99
import * as ws from 'ws';
@@ -166,7 +166,9 @@ export async function handleReactiveStream<Context>(
166166
responder
167167
});
168168
if (!isAuthorized.authorized) {
169-
return exitWithError(new errors.AuthorizationError(isAuthorized.errors));
169+
return exitWithError(
170+
isAuthorized.error ?? new errors.AuthorizationError2(ErrorCode.PSYNC_S2101, 'Authorization failed')
171+
);
170172
}
171173
}
172174

packages/service-core/src/routes/auth.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as auth from '../auth/auth-index.js';
44
import { ServiceContext } from '../system/ServiceContext.js';
55
import * as util from '../util/util-index.js';
66
import { BasicRouterRequest, Context, RequestEndpointHandlerPayload } from './router.js';
7+
import { AuthorizationError2, AuthorizationResponse, ErrorCode, ServiceError } from '@powersync/lib-services-framework';
78

89
export function endpoint(req: BasicRouterRequest) {
910
const protocol = req.headers['x-forwarded-proto'] ?? req.protocol;
@@ -95,25 +96,25 @@ export function getTokenFromHeader(authHeader: string = ''): string | null {
9596
return token ?? null;
9697
}
9798

98-
export const authUser = async (payload: RequestEndpointHandlerPayload) => {
99+
export const authUser = async (payload: RequestEndpointHandlerPayload): Promise<AuthorizationResponse> => {
99100
return authorizeUser(payload.context, payload.request.headers.authorization as string);
100101
};
101102

102-
export async function authorizeUser(context: Context, authHeader: string = '') {
103+
export async function authorizeUser(context: Context, authHeader: string = ''): Promise<AuthorizationResponse> {
103104
const token = getTokenFromHeader(authHeader);
104105
if (token == null) {
105106
return {
106107
authorized: false,
107-
errors: ['Authentication required']
108+
error: new AuthorizationError2(ErrorCode.PSYNC_S2115, 'Authentication required')
108109
};
109110
}
110111

111-
const { context: tokenContext, errors } = await generateContext(context.service_context, token);
112+
const { context: tokenContext, tokenError } = await generateContext(context.service_context, token);
112113

113114
if (!tokenContext) {
114115
return {
115116
authorized: false,
116-
errors
117+
error: tokenError
117118
};
118119
}
119120

@@ -138,10 +139,20 @@ export async function generateContext(serviceContext: ServiceContext, token: str
138139
}
139140
};
140141
} catch (err) {
141-
return {
142-
context: null,
143-
errors: [err.message]
144-
};
142+
if (err instanceof ServiceError) {
143+
return {
144+
context: null,
145+
tokenError: err
146+
};
147+
} else {
148+
return {
149+
context: null,
150+
tokenError: new AuthorizationError2(ErrorCode.PSYNC_S2101, 'Authentication error', {
151+
details: err.message,
152+
cause: err
153+
})
154+
};
155+
}
145156
}
146157
}
147158

packages/service-core/src/routes/configure-rsocket.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { deserialize } from 'bson';
22
import * as http from 'http';
33

4-
import { errors, logger } from '@powersync/lib-services-framework';
4+
import { ErrorCode, errors, logger } from '@powersync/lib-services-framework';
55
import { ReactiveSocketRouter, RSocketRequestMeta } from '@powersync/service-rsocket-router';
66

77
import { ServiceContext } from '../system/ServiceContext.js';
@@ -22,19 +22,19 @@ export function configureRSocket(router: ReactiveSocketRouter<Context>, options:
2222
const { route_generators = DEFAULT_SOCKET_ROUTES, server, service_context } = options;
2323

2424
router.applyWebSocketEndpoints(server, {
25-
contextProvider: async (data: Buffer) => {
25+
contextProvider: async (data: Buffer): Promise<Context & { token: string }> => {
2626
const { token, user_agent } = RSocketContextMeta.decode(deserialize(data) as any);
2727

2828
if (!token) {
29-
throw new errors.AuthorizationError('No token provided');
29+
throw new errors.AuthorizationError2(ErrorCode.PSYNC_S2115, 'No token provided');
3030
}
3131

3232
try {
3333
const extracted_token = getTokenFromHeader(token);
3434
if (extracted_token != null) {
35-
const { context, errors: token_errors } = await generateContext(options.service_context, extracted_token);
35+
const { context, tokenError } = await generateContext(options.service_context, extracted_token);
3636
if (context?.token_payload == null) {
37-
throw new errors.AuthorizationError(token_errors ?? 'Authentication required');
37+
throw new errors.AuthorizationError2(ErrorCode.PSYNC_S2115, 'Authentication required');
3838
}
3939

4040
if (!service_context.routerEngine) {
@@ -45,11 +45,12 @@ export function configureRSocket(router: ReactiveSocketRouter<Context>, options:
4545
token,
4646
user_agent,
4747
...context,
48-
token_errors: token_errors,
48+
token_error: tokenError,
4949
service_context: service_context as RouterServiceContext
5050
};
5151
} else {
52-
throw new errors.AuthorizationError('No token provided');
52+
// Token field is present, but did not contain a token.
53+
throw new errors.AuthorizationError2(ErrorCode.PSYNC_S2115, 'No valid token provided');
5354
}
5455
} catch (ex) {
5556
logger.error(ex);

packages/service-core/src/routes/router.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { router } from '@powersync/lib-services-framework';
1+
import { router, ServiceError } from '@powersync/lib-services-framework';
22
import type { JwtPayload } from '../auth/auth-index.js';
33
import { ServiceContext } from '../system/ServiceContext.js';
44
import { RouterEngine } from './RouterEngine.js';
@@ -16,7 +16,7 @@ export type Context = {
1616
service_context: RouterServiceContext;
1717

1818
token_payload?: JwtPayload;
19-
token_errors?: string[];
19+
token_error?: ServiceError;
2020
/**
2121
* Only on websocket endpoints.
2222
*/

packages/service-errors/src/codes.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,39 @@ export enum ErrorCode {
306306
*/
307307
PSYNC_S2101 = 'PSYNC_S2101',
308308

309+
/**
310+
* Could not verify the auth token signature.
311+
*
312+
* Typical causes include:
313+
* 1. Token kid is not found in the keystore.
314+
* 2. Signature does not match the kid in the keystore.
315+
*/
316+
PSYNC_S2111 = 'PSYNC_S2111',
317+
318+
/**
319+
* Token has expired. Check the expiry date on the token.
320+
*/
321+
PSYNC_S2112 = 'PSYNC_S2112',
322+
323+
/**
324+
* Token expiration date is too long. Issue shorter-lived tokens.
325+
*/
326+
PSYNC_S2113 = 'PSYNC_S2113',
327+
328+
/**
329+
* Token audience does not match expected values.
330+
*
331+
* Check the aud value on the token, compared to the audience values allowed in the service config.
332+
*/
333+
PSYNC_S2114 = 'PSYNC_S2114',
334+
335+
/**
336+
* No token provided. An auth token is required for every request.
337+
*
338+
* The Auhtorization header must start with "Token" or "Bearer", followed by the JWT.
339+
*/
340+
PSYNC_S2115 = 'PSYNC_S2115',
341+
309342
// ## PSYNC_S22xx: Auth integration errors
310343

311344
/**

packages/service-errors/src/errors.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,22 @@ export class AuthorizationError extends ServiceError {
172172
}
173173
}
174174

175+
export class AuthorizationError2 extends ServiceError {
176+
constructor(
177+
code: ErrorCode,
178+
description: string,
179+
options?: Partial<ErrorData> & { sensitiveDetails?: string; cause?: any }
180+
) {
181+
super({
182+
code,
183+
status: 401,
184+
description,
185+
...options
186+
});
187+
this.cause = this.cause;
188+
}
189+
}
190+
175191
export class InternalServerError extends ServiceError {
176192
static readonly CODE = ErrorCode.PSYNC_S2001;
177193

0 commit comments

Comments
 (0)