@absolutejs/auth 0.2.4 → 0.2.5
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.
- package/.prettierignore +4 -0
- package/.prettierrc.json +9 -0
- package/dist/index.js +1 -1
- package/package.json +2 -8
- package/src/authorize.ts +119 -0
- package/src/callback.ts +127 -0
- package/src/constants.ts +13 -0
- package/src/index.ts +88 -0
- package/src/logout.ts +39 -0
- package/src/protectRoute.ts +33 -0
- package/src/providers.ts +109 -0
- package/src/refresh.ts +63 -0
- package/src/revoke.ts +61 -0
- package/src/sessionStore.ts +10 -0
- package/src/status.ts +100 -0
- package/src/typeGuards.ts +32 -0
- package/src/types.ts +78 -0
- package/src/utils.ts +55 -0
- package/tsconfig.json +17 -0
- package/dist/example/components/AuthOptions.d.ts +0 -1
- package/dist/example/components/Example.d.ts +0 -1
- package/dist/example/components/Head.d.ts +0 -6
- package/dist/example/components/Modal.d.ts +0 -8
- package/dist/example/components/Navbar.d.ts +0 -10
- package/dist/example/components/NotAuthorized.d.ts +0 -1
- package/dist/example/components/Protected.d.ts +0 -1
- package/dist/example/db/migrate.d.ts +0 -1
- package/dist/example/db/schema.d.ts +0 -248
- package/dist/example/hooks/useAuthStatus.d.ts +0 -19
- package/dist/example/indexes/ExampleIndex.d.ts +0 -1
- package/dist/example/indexes/NotAuthorizedIndex.d.ts +0 -1
- package/dist/example/indexes/ProtectedIndex.d.ts +0 -1
- package/dist/example/server.d.ts +0 -1
- package/dist/example/utils/networking.d.ts +0 -1
- package/dist/example/utils/pageUtils.d.ts +0 -2
- package/dist/example/utils/styles.d.ts +0 -15
- package/dist/example/utils/userUtils.d.ts +0 -36
package/src/refresh.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { isRefreshableProvider } from './typeGuards';
|
|
3
|
+
import { ClientProviders } from './types';
|
|
4
|
+
|
|
5
|
+
type RefreshProps = {
|
|
6
|
+
clientProviders: ClientProviders;
|
|
7
|
+
refreshRoute?: string;
|
|
8
|
+
onRefresh?: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const refresh = ({
|
|
12
|
+
clientProviders,
|
|
13
|
+
refreshRoute = 'refresh',
|
|
14
|
+
onRefresh
|
|
15
|
+
}: RefreshProps) =>
|
|
16
|
+
new Elysia().post(
|
|
17
|
+
`/${refreshRoute}`,
|
|
18
|
+
async ({ error, cookie: { user_refresh_token, auth_provider } }) => {
|
|
19
|
+
if (user_refresh_token.value === undefined) {
|
|
20
|
+
return error('Unauthorized', 'No refresh token found');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (auth_provider.value === undefined) {
|
|
24
|
+
return error('Unauthorized', 'No auth provider found');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const normalizedProvider = auth_provider.value.toLowerCase();
|
|
28
|
+
const { providerInstance } = clientProviders[normalizedProvider];
|
|
29
|
+
|
|
30
|
+
if (!isRefreshableProvider(providerInstance)) {
|
|
31
|
+
return error('Not Implemented', 'Provider is not refreshable');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
//consider passing tokens to onRefresh
|
|
36
|
+
// const tokens = await providerInstance.refreshAccessToken(
|
|
37
|
+
// user_refresh_token.value
|
|
38
|
+
// );
|
|
39
|
+
|
|
40
|
+
await providerInstance.refreshAccessToken(
|
|
41
|
+
user_refresh_token.value
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
onRefresh?.();
|
|
45
|
+
|
|
46
|
+
return new Response('Token refreshed', {
|
|
47
|
+
status: 204
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err instanceof Error) {
|
|
51
|
+
return error(
|
|
52
|
+
'Internal Server Error',
|
|
53
|
+
`Failed to refresh token: ${err.message}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return error(
|
|
58
|
+
'Internal Server Error',
|
|
59
|
+
`Faile to refresh token: Unknown error: ${err}`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
package/src/revoke.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { isRevocableProvider } from './typeGuards';
|
|
3
|
+
import { ClientProviders } from './types';
|
|
4
|
+
|
|
5
|
+
type RevokeProps = {
|
|
6
|
+
clientProviders: ClientProviders;
|
|
7
|
+
revokeRoute?: string;
|
|
8
|
+
onRevoke?: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const revoke = ({
|
|
12
|
+
clientProviders,
|
|
13
|
+
revokeRoute = 'revoke',
|
|
14
|
+
onRevoke
|
|
15
|
+
}: RevokeProps) =>
|
|
16
|
+
new Elysia().post(
|
|
17
|
+
`/${revokeRoute}/access-token`,
|
|
18
|
+
async ({ error, cookie: { user_refresh_token, auth_provider } }) => {
|
|
19
|
+
if (user_refresh_token.value === undefined) {
|
|
20
|
+
return error('Unauthorized', 'No refresh token found');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (auth_provider.value === undefined) {
|
|
24
|
+
return error('Unauthorized', 'No auth provider found');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const normalizedProvider = auth_provider.value.toLowerCase();
|
|
28
|
+
const { providerInstance } = clientProviders[normalizedProvider];
|
|
29
|
+
|
|
30
|
+
if (!isRevocableProvider(providerInstance)) {
|
|
31
|
+
return error(
|
|
32
|
+
'Not Implemented',
|
|
33
|
+
'Provider does not support revocation'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await providerInstance.revokeAccessToken(
|
|
39
|
+
user_refresh_token.value
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
onRevoke?.();
|
|
43
|
+
|
|
44
|
+
return new Response('Token revoked', {
|
|
45
|
+
status: 204
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err instanceof Error) {
|
|
49
|
+
return error(
|
|
50
|
+
'Internal Server Error',
|
|
51
|
+
`Failed to revoke token: ${err.message}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return error(
|
|
56
|
+
'Internal Server Error',
|
|
57
|
+
`Failed to revoke token: Unknown error: ${err}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { SessionRecord } from './types';
|
|
3
|
+
|
|
4
|
+
export const sessionStore = <UserType>() => {
|
|
5
|
+
const initialSession: SessionRecord<UserType> = {};
|
|
6
|
+
|
|
7
|
+
return new Elysia({ name: 'sessionStore' }).state({
|
|
8
|
+
session: initialSession
|
|
9
|
+
});
|
|
10
|
+
};
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { sessionStore } from './sessionStore';
|
|
3
|
+
import { isRefreshableProvider } from './typeGuards';
|
|
4
|
+
import { ClientProviders } from './types';
|
|
5
|
+
|
|
6
|
+
type StatusProps = {
|
|
7
|
+
clientProviders: ClientProviders;
|
|
8
|
+
statusRoute?: string;
|
|
9
|
+
onStatus?: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const status = <UserType>({
|
|
13
|
+
clientProviders,
|
|
14
|
+
statusRoute = 'auth-status',
|
|
15
|
+
onStatus
|
|
16
|
+
}: StatusProps) =>
|
|
17
|
+
new Elysia()
|
|
18
|
+
.use(sessionStore<UserType>())
|
|
19
|
+
.get(
|
|
20
|
+
`/${statusRoute}`,
|
|
21
|
+
async ({
|
|
22
|
+
error,
|
|
23
|
+
cookie: { user_session_id, auth_provider },
|
|
24
|
+
store: { session }
|
|
25
|
+
}) => {
|
|
26
|
+
try {
|
|
27
|
+
if (user_session_id.value === undefined) {
|
|
28
|
+
return new Response(
|
|
29
|
+
JSON.stringify({ isLoggedIn: false, user: null }),
|
|
30
|
+
{
|
|
31
|
+
headers: { 'Content-Type': 'application/json' }
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (auth_provider.value === undefined) {
|
|
37
|
+
return new Response(
|
|
38
|
+
JSON.stringify({ isLoggedIn: false, user: null }),
|
|
39
|
+
{
|
|
40
|
+
headers: { 'Content-Type': 'application/json' }
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const normalizedProvider =
|
|
46
|
+
auth_provider.value.toLowerCase();
|
|
47
|
+
const { providerInstance } =
|
|
48
|
+
clientProviders[normalizedProvider];
|
|
49
|
+
|
|
50
|
+
// Returns an error if the provider is not refreshable but
|
|
51
|
+
// consider another approach to be more inclusive of providers
|
|
52
|
+
if (!isRefreshableProvider(providerInstance)) {
|
|
53
|
+
return error(
|
|
54
|
+
'Not Implemented',
|
|
55
|
+
'Provider is not refreshable'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const userSession = session[user_session_id.value];
|
|
60
|
+
// console.log(
|
|
61
|
+
// 'Session:',
|
|
62
|
+
// session,
|
|
63
|
+
// 'User Session ID:',
|
|
64
|
+
// user_session_id.value
|
|
65
|
+
// );
|
|
66
|
+
// Return null because the user is not logged in, its not an error just a status
|
|
67
|
+
if (userSession === undefined) {
|
|
68
|
+
return new Response(
|
|
69
|
+
JSON.stringify({ isLoggedIn: false, user: null }),
|
|
70
|
+
{
|
|
71
|
+
headers: { 'Content-Type': 'application/json' }
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { user } = userSession;
|
|
77
|
+
|
|
78
|
+
onStatus?.();
|
|
79
|
+
|
|
80
|
+
return new Response(
|
|
81
|
+
JSON.stringify({ isLoggedIn: true, user }),
|
|
82
|
+
{
|
|
83
|
+
headers: { 'Content-Type': 'application/json' }
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err instanceof Error) {
|
|
88
|
+
return error(
|
|
89
|
+
'Internal Server Error',
|
|
90
|
+
`Error: ${err.message} - ${err.stack ?? ''}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return error(
|
|
95
|
+
'Internal Server Error',
|
|
96
|
+
`Unknown Error: ${err}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { OAuth2Tokens } from 'arctic';
|
|
2
|
+
import { providers } from './providers';
|
|
3
|
+
import { Providers } from './types';
|
|
4
|
+
|
|
5
|
+
export const isRefreshableProvider = (
|
|
6
|
+
provider: InstanceType<(typeof providers)[Providers]>
|
|
7
|
+
): provider is InstanceType<(typeof providers)[Providers]> & {
|
|
8
|
+
refreshAccessToken: () => Promise<OAuth2Tokens>;
|
|
9
|
+
} =>
|
|
10
|
+
'refreshAccessToken' in provider &&
|
|
11
|
+
typeof provider.refreshAccessToken === 'function';
|
|
12
|
+
|
|
13
|
+
export const isRevocableProvider = (
|
|
14
|
+
provider: InstanceType<(typeof providers)[Providers]>
|
|
15
|
+
): provider is InstanceType<(typeof providers)[Providers]> & {
|
|
16
|
+
revokeAccessToken: (token: string) => Promise<void>;
|
|
17
|
+
} =>
|
|
18
|
+
'revokeAccessToken' in provider &&
|
|
19
|
+
typeof provider.revokeAccessToken === 'function';
|
|
20
|
+
|
|
21
|
+
export const isValidProviderKey = (
|
|
22
|
+
provider: string
|
|
23
|
+
): provider is keyof typeof providers =>
|
|
24
|
+
Object.keys(providers)
|
|
25
|
+
.map((key) => key.toLowerCase())
|
|
26
|
+
.includes(provider.toLowerCase());
|
|
27
|
+
|
|
28
|
+
export const isValidUser = <UserType>(user: unknown): user is UserType => true;
|
|
29
|
+
|
|
30
|
+
export const isNonEmptyString = (
|
|
31
|
+
str: string | null | undefined
|
|
32
|
+
): str is string => str !== null && str !== undefined && str.trim() !== '';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Cookie } from 'elysia';
|
|
2
|
+
import { providers } from './providers';
|
|
3
|
+
|
|
4
|
+
type SessionData<UserType> = {
|
|
5
|
+
user: UserType;
|
|
6
|
+
expiresAt: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type Oauth2ConfigOptions = {
|
|
10
|
+
[K in Providers]?: {
|
|
11
|
+
credentials: ConstructorParameters<(typeof providers)[K]>;
|
|
12
|
+
scopes?: string[];
|
|
13
|
+
searchParams?: [string, string][];
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type Providers = keyof typeof providers;
|
|
18
|
+
|
|
19
|
+
export type SessionRecord<UserType> = Record<
|
|
20
|
+
string,
|
|
21
|
+
SessionData<UserType> | undefined
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export type UserFunctionProps = {
|
|
25
|
+
authProvider: string;
|
|
26
|
+
decodedIdToken: {
|
|
27
|
+
[key: string]: string | undefined;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type CreateUser<UserType> = ({
|
|
32
|
+
decodedIdToken,
|
|
33
|
+
authProvider
|
|
34
|
+
}: UserFunctionProps) => Promise<UserType>;
|
|
35
|
+
|
|
36
|
+
export type GetUser<UserType> = ({
|
|
37
|
+
decodedIdToken,
|
|
38
|
+
authProvider
|
|
39
|
+
}: UserFunctionProps) => Promise<UserType | null>;
|
|
40
|
+
|
|
41
|
+
export type OnCallback<UserType> = ({
|
|
42
|
+
authProvider,
|
|
43
|
+
decodedIdToken,
|
|
44
|
+
session,
|
|
45
|
+
user_session_id
|
|
46
|
+
}: {
|
|
47
|
+
authProvider: string;
|
|
48
|
+
decodedIdToken: {
|
|
49
|
+
[key: string]: string | undefined;
|
|
50
|
+
};
|
|
51
|
+
session: SessionRecord<UserType>;
|
|
52
|
+
user_session_id: Cookie<string | undefined>;
|
|
53
|
+
}) => void | Promise<void>;
|
|
54
|
+
|
|
55
|
+
export type AbsoluteAuthProps<UserType> = {
|
|
56
|
+
config: Oauth2ConfigOptions;
|
|
57
|
+
authorizeRoute?: string;
|
|
58
|
+
callbackRoute?: string;
|
|
59
|
+
refreshRoute?: string;
|
|
60
|
+
revokeRoute?: string;
|
|
61
|
+
logoutRoute?: string;
|
|
62
|
+
statusRoute?: string;
|
|
63
|
+
onAuthorize?: () => void;
|
|
64
|
+
onCallback?: OnCallback<UserType>;
|
|
65
|
+
onStatus?: () => void;
|
|
66
|
+
onRefresh?: () => void;
|
|
67
|
+
onLogout?: () => void;
|
|
68
|
+
onRevoke?: () => void;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type ClientProviders = Record<
|
|
72
|
+
string,
|
|
73
|
+
{
|
|
74
|
+
providerInstance: InstanceType<(typeof providers)[Providers]>;
|
|
75
|
+
scopes: string[];
|
|
76
|
+
searchParams: [string, string][];
|
|
77
|
+
}
|
|
78
|
+
>;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Cookie } from 'elysia';
|
|
2
|
+
import { MILLISECONDS_IN_A_DAY } from './constants';
|
|
3
|
+
import { AbsoluteAuthProps, CreateUser, GetUser, SessionRecord } from './types';
|
|
4
|
+
import { isValidUser } from './typeGuards';
|
|
5
|
+
|
|
6
|
+
type InsantiateUserSessionProps<UserType> = {
|
|
7
|
+
authProvider: string;
|
|
8
|
+
decodedIdToken: {
|
|
9
|
+
[key: string]: string | undefined;
|
|
10
|
+
};
|
|
11
|
+
session: SessionRecord<UserType>;
|
|
12
|
+
user_session_id: Cookie<string | undefined>;
|
|
13
|
+
createUser?: CreateUser<UserType>;
|
|
14
|
+
getUser?: GetUser<UserType>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const instantiateUserSession = async <UserType>({
|
|
18
|
+
authProvider,
|
|
19
|
+
decodedIdToken,
|
|
20
|
+
user_session_id,
|
|
21
|
+
session,
|
|
22
|
+
getUser,
|
|
23
|
+
createUser
|
|
24
|
+
}: InsantiateUserSessionProps<UserType>) => {
|
|
25
|
+
let user = await getUser?.({
|
|
26
|
+
authProvider,
|
|
27
|
+
decodedIdToken
|
|
28
|
+
});
|
|
29
|
+
user = user ?? (await createUser?.({ authProvider, decodedIdToken }));
|
|
30
|
+
|
|
31
|
+
// TODO : See if theres a better way to check valid user and not throw an error
|
|
32
|
+
if (!isValidUser<UserType>(user))
|
|
33
|
+
throw new Error('Internal Server Error - Invalid user schema');
|
|
34
|
+
|
|
35
|
+
const sessionKey = crypto.randomUUID();
|
|
36
|
+
|
|
37
|
+
session[sessionKey] = {
|
|
38
|
+
expiresAt: Date.now() + MILLISECONDS_IN_A_DAY,
|
|
39
|
+
user
|
|
40
|
+
};
|
|
41
|
+
console.log('in func user_session_id before set', user_session_id.value);
|
|
42
|
+
|
|
43
|
+
user_session_id.set({
|
|
44
|
+
httpOnly: true,
|
|
45
|
+
sameSite: 'lax',
|
|
46
|
+
secure: true,
|
|
47
|
+
value: sessionKey
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.log('in func user_session_id after set', user_session_id.value);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const createAuthConfig = <UserType>(
|
|
54
|
+
props: AbsoluteAuthProps<UserType>
|
|
55
|
+
) => props;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"lib": ["DOM", "DOM.Iterable", "ESNext"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*", "example/**/*"],
|
|
16
|
+
"exclude": ["node_modules"]
|
|
17
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const AuthOptions: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const Example: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { Dispatch, SetStateAction } from 'react';
|
|
2
|
-
import type { User } from '../db/schema';
|
|
3
|
-
type NavbarProps = {
|
|
4
|
-
user: User | undefined;
|
|
5
|
-
modalOpen: boolean;
|
|
6
|
-
handleLogOut: () => Promise<void>;
|
|
7
|
-
setModalOpen: Dispatch<SetStateAction<boolean>>;
|
|
8
|
-
};
|
|
9
|
-
export declare const Navbar: ({ user, handleLogOut, modalOpen, setModalOpen }: NavbarProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const NotAuthorized: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const Protected: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|