@hammadj/better-auth-scim 1.5.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ import type { Status } from "better-auth";
2
+ import { APIError } from "better-auth";
3
+ import { statusCodes } from "better-call";
4
+
5
+ /**
6
+ * SCIM compliant error
7
+ * See: https://datatracker.ietf.org/doc/html/rfc7644#section-3.12
8
+ */
9
+ export class SCIMAPIError extends APIError {
10
+ constructor(
11
+ status: keyof typeof statusCodes | Status = "INTERNAL_SERVER_ERROR",
12
+ overrides: any = {},
13
+ ) {
14
+ const body = {
15
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
16
+ status: (typeof status === "number"
17
+ ? status
18
+ : statusCodes[status]
19
+ ).toString(),
20
+ detail: overrides.detail,
21
+ ...overrides,
22
+ };
23
+ super(status, body);
24
+ this.message = body.detail ?? body.message;
25
+ }
26
+ }
27
+
28
+ const SCIMErrorOpenAPISchema = {
29
+ type: "object",
30
+ properties: {
31
+ schemas: {
32
+ type: "array",
33
+ items: { type: "string" },
34
+ },
35
+ status: {
36
+ type: "string",
37
+ },
38
+ detail: {
39
+ type: "string",
40
+ },
41
+ scimType: {
42
+ type: "string",
43
+ },
44
+ },
45
+ } as const;
46
+
47
+ export const SCIMErrorOpenAPISchemas = {
48
+ "400": {
49
+ description:
50
+ "Bad Request. Usually due to missing parameters, or invalid parameters",
51
+ content: {
52
+ "application/json": {
53
+ schema: SCIMErrorOpenAPISchema,
54
+ },
55
+ },
56
+ },
57
+ "401": {
58
+ description: "Unauthorized. Due to missing or invalid authentication.",
59
+ content: {
60
+ "application/json": {
61
+ schema: SCIMErrorOpenAPISchema,
62
+ },
63
+ },
64
+ },
65
+ "403": {
66
+ description: "Unauthorized. Due to missing or invalid authentication.",
67
+ content: {
68
+ "application/json": {
69
+ schema: SCIMErrorOpenAPISchema,
70
+ },
71
+ },
72
+ },
73
+ "404": {
74
+ description: "Not Found. The requested resource was not found.",
75
+ content: {
76
+ "application/json": {
77
+ schema: SCIMErrorOpenAPISchema,
78
+ },
79
+ },
80
+ },
81
+ "429": {
82
+ description:
83
+ "Too Many Requests. You have exceeded the rate limit. Try again later.",
84
+ content: {
85
+ "application/json": {
86
+ schema: SCIMErrorOpenAPISchema,
87
+ },
88
+ },
89
+ },
90
+ "500": {
91
+ description:
92
+ "Internal Server Error. This is a problem with the server that you cannot fix.",
93
+ content: {
94
+ "application/json": {
95
+ schema: SCIMErrorOpenAPISchema,
96
+ },
97
+ },
98
+ },
99
+ };
@@ -0,0 +1,69 @@
1
+ import { SCIMUserResourceSchema } from "./user-schemas";
2
+
3
+ export type DBFilter = {
4
+ field: string;
5
+ value: string | string[];
6
+ operator?: any;
7
+ };
8
+
9
+ const SCIMOperators: Record<string, string | undefined> = {
10
+ eq: "eq",
11
+ };
12
+
13
+ const SCIMUserAttributes: Record<string, string | undefined> = {
14
+ userName: "email",
15
+ };
16
+
17
+ export class SCIMParseError extends Error {}
18
+
19
+ const SCIMFilterRegex =
20
+ /^\s*(?<attribute>[^\s]+)\s+(?<op>eq|ne|co|sw|ew|pr)\s*(?:(?<value>"[^"]*"|[^\s]+))?\s*$/i;
21
+
22
+ const parseSCIMFilter = (filter: string) => {
23
+ const match = filter.match(SCIMFilterRegex);
24
+ if (!match) {
25
+ throw new SCIMParseError("Invalid filter expression");
26
+ }
27
+
28
+ const attribute = match.groups?.attribute;
29
+ const op = match.groups?.op?.toLowerCase();
30
+ const value = match.groups?.value;
31
+
32
+ if (!attribute || !op || !value) {
33
+ throw new SCIMParseError("Invalid filter expression");
34
+ }
35
+
36
+ const operator = SCIMOperators[op];
37
+ if (!operator) {
38
+ throw new SCIMParseError(`The operator "${op}" is not supported`);
39
+ }
40
+
41
+ return { attribute, operator, value };
42
+ };
43
+
44
+ export const parseSCIMUserFilter = (filter: string) => {
45
+ const { attribute, operator, value } = parseSCIMFilter(filter);
46
+
47
+ const filters: DBFilter[] = [];
48
+ const targetAttribute = SCIMUserAttributes[attribute];
49
+ const resourceAttribute = SCIMUserResourceSchema.attributes.find(
50
+ (attr) => attr.name === attribute,
51
+ );
52
+
53
+ if (!targetAttribute || !resourceAttribute) {
54
+ throw new SCIMParseError(`The attribute "${attribute}" is not supported`);
55
+ }
56
+
57
+ let finalValue = value.replaceAll('"', "");
58
+ if (!resourceAttribute.caseExact) {
59
+ finalValue = finalValue.toLowerCase();
60
+ }
61
+
62
+ filters.push({
63
+ field: targetAttribute,
64
+ value: finalValue,
65
+ operator,
66
+ });
67
+
68
+ return filters;
69
+ };
@@ -0,0 +1,128 @@
1
+ const MetadataFieldSupportOpenAPISchema = {
2
+ type: "object",
3
+ properties: {
4
+ supported: {
5
+ type: "boolean",
6
+ },
7
+ },
8
+ };
9
+
10
+ export const ServiceProviderOpenAPISchema = {
11
+ type: "object",
12
+ properties: {
13
+ patch: MetadataFieldSupportOpenAPISchema,
14
+ bulk: MetadataFieldSupportOpenAPISchema,
15
+ filter: MetadataFieldSupportOpenAPISchema,
16
+ changePassword: MetadataFieldSupportOpenAPISchema,
17
+ sort: MetadataFieldSupportOpenAPISchema,
18
+ etag: MetadataFieldSupportOpenAPISchema,
19
+ authenticationSchemes: {
20
+ type: "array",
21
+ items: {
22
+ type: "object",
23
+ properties: {
24
+ name: {
25
+ type: "string",
26
+ },
27
+ description: {
28
+ type: "string",
29
+ },
30
+ specUri: {
31
+ type: "string",
32
+ },
33
+ type: {
34
+ type: "string",
35
+ },
36
+ primary: {
37
+ type: "boolean",
38
+ },
39
+ },
40
+ },
41
+ },
42
+ schemas: {
43
+ type: "array",
44
+ items: {
45
+ type: "string",
46
+ },
47
+ },
48
+ meta: {
49
+ type: "object",
50
+ properties: {
51
+ resourceType: {
52
+ type: "string",
53
+ },
54
+ },
55
+ },
56
+ },
57
+ } as const;
58
+
59
+ export const ResourceTypeOpenAPISchema = {
60
+ type: "object",
61
+ properties: {
62
+ schemas: {
63
+ type: "array",
64
+ items: { type: "string" },
65
+ },
66
+ id: { type: "string" },
67
+ name: { type: "string" },
68
+ endpoint: { type: "string" },
69
+ description: { type: "string" },
70
+ schema: { type: "string" },
71
+ meta: {
72
+ type: "object",
73
+ properties: {
74
+ resourceType: { type: "string" },
75
+ location: { type: "string" },
76
+ },
77
+ },
78
+ },
79
+ } as const;
80
+
81
+ const SCIMSchemaAttributesOpenAPISchema = {
82
+ type: "object",
83
+ properties: {
84
+ name: { type: "string" },
85
+ type: { type: "string" },
86
+ multiValued: { type: "boolean" },
87
+ description: { type: "string" },
88
+ required: { type: "boolean" },
89
+ caseExact: { type: "boolean" },
90
+ mutability: { type: "string" },
91
+ returned: { type: "string" },
92
+ uniqueness: { type: "string" },
93
+ },
94
+ } as const;
95
+
96
+ export const SCIMSchemaOpenAPISchema = {
97
+ type: "object",
98
+ properties: {
99
+ id: { type: "string" },
100
+ schemas: {
101
+ type: "array",
102
+ items: { type: "string" },
103
+ },
104
+ name: { type: "string" },
105
+ description: { type: "string" },
106
+ attributes: {
107
+ type: "array",
108
+ items: {
109
+ ...SCIMSchemaAttributesOpenAPISchema,
110
+ properties: {
111
+ ...SCIMSchemaAttributesOpenAPISchema.properties,
112
+ subAttributes: {
113
+ type: "array",
114
+ items: SCIMSchemaAttributesOpenAPISchema,
115
+ },
116
+ },
117
+ },
118
+ },
119
+ meta: {
120
+ type: "object",
121
+ properties: {
122
+ resourceType: { type: "string" },
123
+ location: { type: "string" },
124
+ },
125
+ required: ["resourceType", "location"],
126
+ },
127
+ },
128
+ } as const;
@@ -0,0 +1,35 @@
1
+ import type { Account, User } from "better-auth";
2
+ import { SCIMUserResourceSchema } from "./user-schemas";
3
+ import { getResourceURL } from "./utils";
4
+
5
+ export const createUserResource = (
6
+ baseURL: string,
7
+ user: User,
8
+ account?: Account | null,
9
+ ) => {
10
+ return {
11
+ // Common attributes
12
+ // See https://datatracker.ietf.org/doc/html/rfc7643#section-3.1
13
+
14
+ id: user.id,
15
+ externalId: account?.accountId,
16
+ meta: {
17
+ resourceType: "User",
18
+ created: user.createdAt,
19
+ lastModified: user.updatedAt,
20
+ location: getResourceURL(`/scim/v2/Users/${user.id}`, baseURL),
21
+ },
22
+
23
+ // SCIM user resource
24
+ // See https://datatracker.ietf.org/doc/html/rfc7643#section-4.1
25
+
26
+ userName: user.email,
27
+ name: {
28
+ formatted: user.name,
29
+ },
30
+ displayName: user.name,
31
+ active: true,
32
+ emails: [{ primary: true, value: user.email }],
33
+ schemas: [SCIMUserResourceSchema.id],
34
+ };
35
+ };
@@ -0,0 +1,71 @@
1
+ import type { GenericEndpointContext } from "better-auth";
2
+ import { symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
3
+ import { defaultKeyHasher } from "better-auth/plugins";
4
+ import type { SCIMOptions } from "./types";
5
+
6
+ export async function storeSCIMToken(
7
+ ctx: GenericEndpointContext,
8
+ opts: SCIMOptions,
9
+ scimToken: string,
10
+ ) {
11
+ if (opts.storeSCIMToken === "encrypted") {
12
+ return await symmetricEncrypt({
13
+ key: ctx.context.secret,
14
+ data: scimToken,
15
+ });
16
+ }
17
+ if (opts.storeSCIMToken === "hashed") {
18
+ return await defaultKeyHasher(scimToken);
19
+ }
20
+ if (
21
+ typeof opts.storeSCIMToken === "object" &&
22
+ "hash" in opts.storeSCIMToken
23
+ ) {
24
+ return await opts.storeSCIMToken.hash(scimToken);
25
+ }
26
+ if (
27
+ typeof opts.storeSCIMToken === "object" &&
28
+ "encrypt" in opts.storeSCIMToken
29
+ ) {
30
+ return await opts.storeSCIMToken.encrypt(scimToken);
31
+ }
32
+
33
+ return scimToken;
34
+ }
35
+
36
+ export async function verifySCIMToken(
37
+ ctx: GenericEndpointContext,
38
+ opts: SCIMOptions,
39
+ storedSCIMToken: string,
40
+ scimToken: string,
41
+ ): Promise<boolean> {
42
+ if (opts.storeSCIMToken === "encrypted") {
43
+ return (
44
+ (await symmetricDecrypt({
45
+ key: ctx.context.secret,
46
+ data: storedSCIMToken,
47
+ })) === scimToken
48
+ );
49
+ }
50
+ if (opts.storeSCIMToken === "hashed") {
51
+ const hashedSCIMToken = await defaultKeyHasher(scimToken);
52
+ return hashedSCIMToken === storedSCIMToken;
53
+ }
54
+ if (
55
+ typeof opts.storeSCIMToken === "object" &&
56
+ "hash" in opts.storeSCIMToken
57
+ ) {
58
+ const hashedSCIMToken = await opts.storeSCIMToken.hash(scimToken);
59
+ return hashedSCIMToken === storedSCIMToken;
60
+ }
61
+ if (
62
+ typeof opts.storeSCIMToken === "object" &&
63
+ "decrypt" in opts.storeSCIMToken
64
+ ) {
65
+ const decryptedSCIMToken =
66
+ await opts.storeSCIMToken.decrypt(storedSCIMToken);
67
+ return decryptedSCIMToken === scimToken;
68
+ }
69
+
70
+ return scimToken === storedSCIMToken;
71
+ }