@scaleway/auth-scw 1.0.0
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/LICENSE.md +21 -0
- package/README.md +69 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +23 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/isExpired.js +5 -0
- package/dist/types/iam/index.d.ts +9 -0
- package/dist/types/iam/types.d.ts +44 -0
- package/dist/types/index.d.ts +54 -0
- package/dist/useAuthScw/AuthScwProvider.d.ts +24 -0
- package/dist/useAuthScw/AuthScwProvider.js +129 -0
- package/dist/useAuthScw/authStoreManager.d.ts +29 -0
- package/dist/useAuthScw/authStoreManager.js +105 -0
- package/dist/useAuthScw/createClient.js +24 -0
- package/dist/useAuthScw/helpers.js +40 -0
- package/dist/useAuthScw/index.d.ts +1 -0
- package/dist/useAuthScw/proxyJwt.js +29 -0
- package/dist/zodSchemas.d.ts +20 -0
- package/dist/zodSchemas.js +19 -0
- package/package.json +51 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Scaleway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Scaleway - Auth Scw
|
|
2
|
+
|
|
3
|
+
This package contains a React Context Provider which will help to handle authentication workflow within an app.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @scaleway/auth-scw
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
## How to dev
|
|
16
|
+
|
|
17
|
+
### Prerequisites
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
cd packages/auth-scw
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm build # Build the package
|
|
27
|
+
pnpm watch # Build the package and watch for changes
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
# Repo pattern
|
|
31
|
+
|
|
32
|
+
## How does renew token works? Single or multiple organisation
|
|
33
|
+
|
|
34
|
+
**Reminder:** A COOKIE is used to syncronise jwt between tabs
|
|
35
|
+
|
|
36
|
+
When users open severals tabs, syncronoisation of token between tabs can become tedious, here are the scenarios:
|
|
37
|
+
|
|
38
|
+
### Single organisation
|
|
39
|
+
|
|
40
|
+
Let say you have 2 tabs on the same organisation, `tab1`, `tab2` with the same JWT, let's call it, `JWT_A`
|
|
41
|
+
|
|
42
|
+
1. you navigate on `tab2` and after 1h renew your `JWT_A` (and update the cookie) with a new jwt called `JWT_B`
|
|
43
|
+
2. now `tab1` has a React state with an expired `JWT_A` and tabs 2 has renewed its jwt with `JWT_B`
|
|
44
|
+
3. you leave for 1h. You come back on the console app after the `JWT_B` on `tab2` has expired
|
|
45
|
+
4. `tab1` AND `tab2` have both a different expired jwt, `JWT_A` and `JWT_B`
|
|
46
|
+
5. you go on `tab1` do an action on the console => a request is sent
|
|
47
|
+
6. The code will check, React state `JWT_A` is expired then check Cookie `JWT_B` is also expired
|
|
48
|
+
|
|
49
|
+
// A long term bug use to be:
|
|
50
|
+
|
|
51
|
+
1. A renew request on `tab1` is attempted with `JWT_A` renew token (which has already been used on step 1)
|
|
52
|
+
2. the API trigger an error telling you (more or less) that this `JWT_A` for renew has already been used (which is fair)
|
|
53
|
+
|
|
54
|
+
// Bug corrected
|
|
55
|
+
|
|
56
|
+
1. A renew request on `tab1` is attempted with the last JWT from cookie (so JWT_B) and also because both tabs are on the same organisation therefore, the same jwt can be used for both.
|
|
57
|
+
2. the renew happen without issue and both tabs can be used without problems
|
|
58
|
+
|
|
59
|
+
### Multiple organisation
|
|
60
|
+
|
|
61
|
+
Alright, read the Single organisation scenario if you haven't.
|
|
62
|
+
Here let's consider we have `tab1` and `tab2` on `Orga_A` with `JWT_A` and `tab3` on `Orga_B` with `JWT_B`:
|
|
63
|
+
|
|
64
|
+
1. the last tab you opened is `tab3`, therefore, your cookie is set with `JWT_B` on `Orga_B`
|
|
65
|
+
2. you navigate on `tab1` after 1h your `JWT_A` is being renewed in `JWT_A2` AND the app will use the React state instead of the cookie to have the renew token from `JWT_A`. Because the cookie hold `JWT_B` which is set on `Orga_B` and you are navigating on `tab1` which is on `Orga_A`
|
|
66
|
+
3. Your renew happens without issue, you keep navigating on `tab1`
|
|
67
|
+
4. if you switch tab and go to `tab2` then the app will attempt a `JWT_A` renew (remember `tab1` and `tab2` used both `JWT_A` that they had in their React state)
|
|
68
|
+
but this time because `JWT_A` has already been used on step 2 to renew, the request will **fail** and tell you `JWT_A` has already been used for renew => this will end up in disconnecting the user
|
|
69
|
+
At the moment we only use the cookie to syncronise tabs on only **ONE** organisation, but other tab which have switched organisation will not work because they are not using the same organisation than the cookie.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/constants.ts
|
|
2
|
+
const EmptySession = {
|
|
3
|
+
jwt: {
|
|
4
|
+
audienceId: "",
|
|
5
|
+
createdAt: void 0,
|
|
6
|
+
expiresAt: void 0,
|
|
7
|
+
ip: "",
|
|
8
|
+
issuerId: "",
|
|
9
|
+
jti: "",
|
|
10
|
+
updatedAt: void 0,
|
|
11
|
+
userAgent: ""
|
|
12
|
+
},
|
|
13
|
+
renewToken: "",
|
|
14
|
+
token: ""
|
|
15
|
+
};
|
|
16
|
+
const COOKIE_CONFIG = {
|
|
17
|
+
httpOnly: false,
|
|
18
|
+
path: "/",
|
|
19
|
+
sameSite: "strict",
|
|
20
|
+
secure: true
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { COOKIE_CONFIG, EmptySession };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { AudienceIdType, AuthScwContextType, EncodedJWT } from "./types/index.js";
|
|
2
|
+
import { EmptySession } from "./constants.js";
|
|
3
|
+
import { AuthScwProvider, useAuthScw } from "./useAuthScw/AuthScwProvider.js";
|
|
4
|
+
import { AuthStoreManager } from "./useAuthScw/authStoreManager.js";
|
|
5
|
+
export { type AudienceIdType, type AuthScwContextType, AuthScwProvider, AuthStoreManager, EmptySession, type EncodedJWT, useAuthScw };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { API, DeleteJWTRequest, EncodedJWT, JWT, UnauthenticatedAPI, UnauthenticatedApiRenewJWTRequest } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/types/iam/index.d.ts
|
|
4
|
+
declare namespace Iamv1alpha1 {
|
|
5
|
+
export type { API, UnauthenticatedAPI };
|
|
6
|
+
export type { DeleteJWTRequest, EncodedJWT, JWT, UnauthenticatedApiRenewJWTRequest };
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { Iamv1alpha1 };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { API } from "@scaleway/sdk-client";
|
|
2
|
+
import { Iamv1alpha1 } from "@scaleway/sdk-iam";
|
|
3
|
+
|
|
4
|
+
//#region src/types/iam/types.d.ts
|
|
5
|
+
type JWT = Iamv1alpha1.JWT;
|
|
6
|
+
type EncodedJWT = Iamv1alpha1.EncodedJWT;
|
|
7
|
+
type DeleteJWTRequest = Iamv1alpha1.DeleteJWTRequest;
|
|
8
|
+
type UnauthenticatedApiRenewJWTRequest = {
|
|
9
|
+
/**
|
|
10
|
+
* The JWT ID of the JWT to renew.
|
|
11
|
+
*/
|
|
12
|
+
jti: string;
|
|
13
|
+
/**
|
|
14
|
+
* The renew token associated to the JWT to renew.
|
|
15
|
+
*/
|
|
16
|
+
renewToken: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* IAM API.
|
|
20
|
+
|
|
21
|
+
This API allows you to manage Identity and Access Management (IAM) across your Scaleway Organizations, Projects and resources.
|
|
22
|
+
*/
|
|
23
|
+
declare class API$1 extends API {
|
|
24
|
+
/**
|
|
25
|
+
* Delete a JWT.
|
|
26
|
+
*
|
|
27
|
+
* @param request - The request {@link DeleteJWTRequest}
|
|
28
|
+
*/
|
|
29
|
+
deleteJWT: (request: Readonly<DeleteJWTRequest>) => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Unauthenticated API.
|
|
33
|
+
*/
|
|
34
|
+
declare class UnauthenticatedAPI extends API {
|
|
35
|
+
/**
|
|
36
|
+
* Renew a JWT.
|
|
37
|
+
*
|
|
38
|
+
* @param request - The request {@link UnauthenticatedApiRenewJWTRequest}
|
|
39
|
+
* @returns A Promise of EncodedJWT
|
|
40
|
+
*/
|
|
41
|
+
renewJWT: (request: Readonly<UnauthenticatedApiRenewJWTRequest>) => Promise<EncodedJWT>;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { API$1 as API, DeleteJWTRequest, EncodedJWT, JWT, UnauthenticatedAPI, UnauthenticatedApiRenewJWTRequest };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Iamv1alpha1 } from "./iam/index.js";
|
|
2
|
+
import { audienceIdSchema, jwtSchema } from "../zodSchemas.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { API, Client, Settings } from "@scaleway/sdk-client";
|
|
5
|
+
|
|
6
|
+
//#region src/types/index.d.ts
|
|
7
|
+
type EncodedJWT = z.infer<typeof jwtSchema>;
|
|
8
|
+
type AudienceIdType = z.infer<typeof audienceIdSchema>;
|
|
9
|
+
type OnError = (onError: {
|
|
10
|
+
title: string;
|
|
11
|
+
error: unknown;
|
|
12
|
+
extra?: Record<string, unknown>;
|
|
13
|
+
}) => void;
|
|
14
|
+
type GetJWT = (audienceId?: string) => Promise<(EncodedJWT & {
|
|
15
|
+
source: 'refreshSession' | 'cookie';
|
|
16
|
+
}) | undefined>;
|
|
17
|
+
type GetJwtTokenResponse = NonNullable<Awaited<ReturnType<GetJWT>>>['token'] | undefined;
|
|
18
|
+
type AuthScwContextType = {
|
|
19
|
+
audienceId?: string;
|
|
20
|
+
setAudienceId: (audienceId: string) => void;
|
|
21
|
+
jti?: string;
|
|
22
|
+
encodeToken: (jwt: EncodedJWT) => string;
|
|
23
|
+
decodeToken: (encodedToken: string) => EncodedJWT | null;
|
|
24
|
+
setJWT: (jwtInfoParam: EncodedJWT) => void;
|
|
25
|
+
getJWT: GetJWT;
|
|
26
|
+
getJwtToken: (audienceId?: string) => Promise<GetJwtTokenResponse>;
|
|
27
|
+
logout: () => void;
|
|
28
|
+
authenticated: boolean;
|
|
29
|
+
};
|
|
30
|
+
declare class IamV1alpha1API extends API {
|
|
31
|
+
deleteJWT: Iamv1alpha1.API['deleteJWT'];
|
|
32
|
+
}
|
|
33
|
+
declare class IamV1alpha1UnauthenticatedAPI extends API {
|
|
34
|
+
renewJWT: Iamv1alpha1.UnauthenticatedAPI['renewJWT'];
|
|
35
|
+
}
|
|
36
|
+
type ConfigurationClientAPIs = {
|
|
37
|
+
IamV1Alpha1: {
|
|
38
|
+
new (client: Client): IamV1alpha1API;
|
|
39
|
+
};
|
|
40
|
+
IamUnauthenticatedV1Alpha1: {
|
|
41
|
+
new (client: Client): IamV1alpha1UnauthenticatedAPI;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
type ConfigAuthProvider = {
|
|
45
|
+
clientSettings: Settings;
|
|
46
|
+
} & ConfigurationClientAPIs;
|
|
47
|
+
type CookieConfigType = {
|
|
48
|
+
httpOnly: boolean;
|
|
49
|
+
path: string;
|
|
50
|
+
sameSite: 'lax' | 'none' | 'strict' | boolean | undefined;
|
|
51
|
+
secure: boolean;
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { AudienceIdType, AuthScwContextType, ConfigAuthProvider, CookieConfigType, EncodedJWT, OnError };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AuthScwContextType, ConfigAuthProvider, CookieConfigType, OnError } from "../types/index.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/useAuthScw/AuthScwProvider.d.ts
|
|
5
|
+
type AuthProviderParamType = ConfigAuthProvider & {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
cookieSuffix: string;
|
|
8
|
+
urlParamTokenName: string;
|
|
9
|
+
onError?: OnError;
|
|
10
|
+
cookieConfig?: CookieConfigType;
|
|
11
|
+
};
|
|
12
|
+
declare const useAuthScw: () => AuthScwContextType;
|
|
13
|
+
declare const AuthScwProvider: ({
|
|
14
|
+
clientSettings,
|
|
15
|
+
IamV1Alpha1,
|
|
16
|
+
IamUnauthenticatedV1Alpha1,
|
|
17
|
+
children,
|
|
18
|
+
cookieSuffix,
|
|
19
|
+
cookieConfig,
|
|
20
|
+
urlParamTokenName,
|
|
21
|
+
onError
|
|
22
|
+
}: AuthProviderParamType) => import("react").JSX.Element;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { AuthScwProvider, useAuthScw };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { isExpired } from "../isExpired.js";
|
|
2
|
+
import { AuthStoreManager, setCookieConfig } from "./authStoreManager.js";
|
|
3
|
+
import { clientSingleton } from "./createClient.js";
|
|
4
|
+
import { decodeToken, encodeToken, getCookieJWT, refreshSession } from "./helpers.js";
|
|
5
|
+
import { proxyJwt } from "./proxyJwt.js";
|
|
6
|
+
import { createContext, useCallback, useContext, useMemo, useState } from "react";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
//#region src/useAuthScw/AuthScwProvider.tsx
|
|
9
|
+
const AuthScwContext = createContext({});
|
|
10
|
+
const useAuthScw = () => useContext(AuthScwContext);
|
|
11
|
+
const AuthScwProvider = ({ clientSettings, IamV1Alpha1, IamUnauthenticatedV1Alpha1, children, cookieSuffix, cookieConfig, urlParamTokenName, onError }) => {
|
|
12
|
+
if (cookieConfig) setCookieConfig(cookieConfig);
|
|
13
|
+
clientSingleton.setAPIsAndSettings({
|
|
14
|
+
clientSettings,
|
|
15
|
+
IamV1Alpha1,
|
|
16
|
+
IamUnauthenticatedV1Alpha1
|
|
17
|
+
});
|
|
18
|
+
const initAudienceId = useCallback(() => {
|
|
19
|
+
AuthStoreManager.setSuffixKey(cookieSuffix);
|
|
20
|
+
const currentUrl = new URL(globalThis.location.href);
|
|
21
|
+
const tokenRaw = currentUrl.searchParams.get(urlParamTokenName);
|
|
22
|
+
if (tokenRaw) {
|
|
23
|
+
currentUrl.searchParams.delete(urlParamTokenName);
|
|
24
|
+
const token = decodeToken(tokenRaw);
|
|
25
|
+
if (token) {
|
|
26
|
+
const jwtProxy = proxyJwt(token);
|
|
27
|
+
if (jwtProxy.jwt) AuthStoreManager.setJwt({ jwtInfo: jwtProxy });
|
|
28
|
+
globalThis.history.replaceState({}, "", currentUrl);
|
|
29
|
+
return jwtProxy.jwt?.audienceId;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const audienceId = AuthStoreManager.getAudienceId();
|
|
33
|
+
if (audienceId) {
|
|
34
|
+
if (AuthStoreManager.getJwt(audienceId)) return audienceId;
|
|
35
|
+
}
|
|
36
|
+
}, [cookieSuffix, urlParamTokenName]);
|
|
37
|
+
const [currentAudienceId, setCurrentAudienceId] = useState(() => initAudienceId());
|
|
38
|
+
const setJWT = useCallback((jwtInfoParam) => {
|
|
39
|
+
const currentJWT = proxyJwt(jwtInfoParam);
|
|
40
|
+
if (currentJWT.jwt?.audienceId) {
|
|
41
|
+
AuthStoreManager.setJwt({ jwtInfo: currentJWT });
|
|
42
|
+
setCurrentAudienceId(currentJWT.jwt.audienceId);
|
|
43
|
+
}
|
|
44
|
+
}, [setCurrentAudienceId]);
|
|
45
|
+
const getJWT = useCallback(async (audienceId = currentAudienceId) => {
|
|
46
|
+
if (audienceId) {
|
|
47
|
+
const cookieJWT = getCookieJWT(audienceId);
|
|
48
|
+
if (cookieJWT?.jwt) {
|
|
49
|
+
const { renewToken, token, jwt: currentJWT } = cookieJWT;
|
|
50
|
+
if (currentJWT.expiresAt && !isExpired(new Date(currentJWT.expiresAt))) return {
|
|
51
|
+
jwt: currentJWT,
|
|
52
|
+
renewToken,
|
|
53
|
+
source: "cookie",
|
|
54
|
+
token
|
|
55
|
+
};
|
|
56
|
+
const jwtInfoData = await refreshSession({
|
|
57
|
+
onError,
|
|
58
|
+
paramsRenewRequest: {
|
|
59
|
+
jti: currentJWT.jti,
|
|
60
|
+
renewToken
|
|
61
|
+
},
|
|
62
|
+
setJWT
|
|
63
|
+
});
|
|
64
|
+
if (jwtInfoData) return {
|
|
65
|
+
jwt: jwtInfoData.jwt,
|
|
66
|
+
renewToken: jwtInfoData.renewToken,
|
|
67
|
+
source: "refreshSession",
|
|
68
|
+
token: jwtInfoData.token
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, [
|
|
73
|
+
currentAudienceId,
|
|
74
|
+
setJWT,
|
|
75
|
+
onError
|
|
76
|
+
]);
|
|
77
|
+
const getJwtToken = useCallback(async (audienceId) => {
|
|
78
|
+
const currentJWT = await getJWT(audienceId);
|
|
79
|
+
if (currentJWT) return currentJWT.token;
|
|
80
|
+
}, [getJWT]);
|
|
81
|
+
const logout = useCallback(() => {
|
|
82
|
+
if (currentAudienceId) {
|
|
83
|
+
const cookieJWT = getCookieJWT(currentAudienceId);
|
|
84
|
+
AuthStoreManager.deleteJwt(currentAudienceId);
|
|
85
|
+
if (cookieJWT?.jwt) {
|
|
86
|
+
const { deleteJWT } = clientSingleton.createClient({ getAsyncToken: getJwtToken });
|
|
87
|
+
deleteJWT?.({ jti: cookieJWT.jwt.jti }).catch(() => null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
AuthStoreManager.deleteAudienceId();
|
|
91
|
+
setCurrentAudienceId(void 0);
|
|
92
|
+
}, [getJwtToken, currentAudienceId]);
|
|
93
|
+
const jti = useMemo(() => {
|
|
94
|
+
if (currentAudienceId) {
|
|
95
|
+
const cookieJWT = getCookieJWT(currentAudienceId);
|
|
96
|
+
if (cookieJWT?.jwt) return cookieJWT.jwt.jti;
|
|
97
|
+
}
|
|
98
|
+
}, [currentAudienceId]);
|
|
99
|
+
const isAuthenticated = useMemo(() => !!(currentAudienceId && getCookieJWT(currentAudienceId)), [currentAudienceId]);
|
|
100
|
+
const setAudienceId = useCallback((audienceId) => {
|
|
101
|
+
setCurrentAudienceId(audienceId);
|
|
102
|
+
}, []);
|
|
103
|
+
return /* @__PURE__ */ jsx(AuthScwContext, {
|
|
104
|
+
value: useMemo(() => ({
|
|
105
|
+
audienceId: currentAudienceId ?? void 0,
|
|
106
|
+
authenticated: isAuthenticated,
|
|
107
|
+
decodeToken,
|
|
108
|
+
encodeToken,
|
|
109
|
+
getJWT,
|
|
110
|
+
getJwtToken,
|
|
111
|
+
jti,
|
|
112
|
+
logout,
|
|
113
|
+
setAudienceId,
|
|
114
|
+
setJWT
|
|
115
|
+
}), [
|
|
116
|
+
getJWT,
|
|
117
|
+
getJwtToken,
|
|
118
|
+
logout,
|
|
119
|
+
setJWT,
|
|
120
|
+
setAudienceId,
|
|
121
|
+
currentAudienceId,
|
|
122
|
+
isAuthenticated,
|
|
123
|
+
jti
|
|
124
|
+
]),
|
|
125
|
+
children
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
129
|
+
export { AuthScwProvider, useAuthScw };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AudienceIdType, EncodedJWT } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/useAuthScw/authStoreManager.d.ts
|
|
4
|
+
declare const AuthStoreManager: {
|
|
5
|
+
COOKIE_AGE: number;
|
|
6
|
+
DOMAIN: string;
|
|
7
|
+
deleteAllJwts(): void;
|
|
8
|
+
deleteAudienceId(): void;
|
|
9
|
+
deleteJwt(audienceId: string): void;
|
|
10
|
+
getAudienceId(): string | null;
|
|
11
|
+
getJwt(audienceId: string): EncodedJWT | null;
|
|
12
|
+
getKeyAudienceId(): string;
|
|
13
|
+
getKeySession(audienceId: string): string;
|
|
14
|
+
getUniqueHostnameString: (inputString: string) => string;
|
|
15
|
+
SUFFIX_KEY: string;
|
|
16
|
+
setAudienceId(audienceId: string): void;
|
|
17
|
+
setJwt({
|
|
18
|
+
jwtInfo,
|
|
19
|
+
setAudienceId
|
|
20
|
+
}: {
|
|
21
|
+
jwtInfo: EncodedJWT;
|
|
22
|
+
setAudienceId?: boolean;
|
|
23
|
+
}): void;
|
|
24
|
+
setSuffixKey(key: string): void;
|
|
25
|
+
typeGuardAudienceId(untypedObject: unknown): untypedObject is AudienceIdType;
|
|
26
|
+
typeGuardJWT(untypedObject: unknown): untypedObject is EncodedJWT;
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { AuthStoreManager };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { COOKIE_CONFIG } from "../constants.js";
|
|
2
|
+
import { audienceIdSchema, jwtSchema } from "../zodSchemas.js";
|
|
3
|
+
import { parseCookie, stringifySetCookie } from "cookie";
|
|
4
|
+
//#region src/useAuthScw/authStoreManager.ts
|
|
5
|
+
const KEY_SESSION = "_scw_session";
|
|
6
|
+
const KEY_AUDIENCE_ID = "_scw_audience_id";
|
|
7
|
+
const DEFAULT_SUFFIX_KEY = "";
|
|
8
|
+
const COOKIE_AGE = 3600 * 24 * 31;
|
|
9
|
+
let cookieConfig = COOKIE_CONFIG;
|
|
10
|
+
const DOMAIN = typeof window !== "undefined" ? globalThis.location.hostname : "";
|
|
11
|
+
const getUniqueHostnameString = (inputString) => inputString.replaceAll(".", "_").replaceAll("-", "_");
|
|
12
|
+
const setCookieConfig = (COOKIE_CONFIG_PARAM) => cookieConfig = COOKIE_CONFIG_PARAM;
|
|
13
|
+
const setCookie = (value, key, maxAge = COOKIE_AGE) => {
|
|
14
|
+
document.cookie = stringifySetCookie({
|
|
15
|
+
name: key,
|
|
16
|
+
value: JSON.stringify(value),
|
|
17
|
+
domain: DOMAIN,
|
|
18
|
+
httpOnly: cookieConfig.httpOnly,
|
|
19
|
+
maxAge,
|
|
20
|
+
path: cookieConfig.path,
|
|
21
|
+
sameSite: cookieConfig.sameSite,
|
|
22
|
+
secure: cookieConfig.secure
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
const getCookie = (key) => {
|
|
26
|
+
const cookies = parseCookie(document.cookie);
|
|
27
|
+
if (key in cookies && cookies[key]) {
|
|
28
|
+
const cookieSessionString = cookies[key];
|
|
29
|
+
if (cookieSessionString) return JSON.parse(cookieSessionString);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
34
|
+
const deleteCookie = (key) => {
|
|
35
|
+
document.cookie = stringifySetCookie({
|
|
36
|
+
name: key,
|
|
37
|
+
value: "",
|
|
38
|
+
domain: DOMAIN,
|
|
39
|
+
httpOnly: cookieConfig.httpOnly,
|
|
40
|
+
maxAge: -1,
|
|
41
|
+
path: cookieConfig.path,
|
|
42
|
+
sameSite: cookieConfig.sameSite,
|
|
43
|
+
secure: cookieConfig.secure
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const AuthStoreManager = {
|
|
47
|
+
COOKIE_AGE,
|
|
48
|
+
DOMAIN,
|
|
49
|
+
deleteAllJwts() {
|
|
50
|
+
const prefix = AuthStoreManager.getKeySession("");
|
|
51
|
+
document.cookie.split(";").forEach((cookieParam) => {
|
|
52
|
+
let [audienceId] = cookieParam.split("=");
|
|
53
|
+
if (audienceId) {
|
|
54
|
+
audienceId = audienceId.trim();
|
|
55
|
+
if (audienceId.startsWith(prefix)) this.deleteJwt(audienceId);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
deleteAudienceId() {
|
|
60
|
+
deleteCookie(AuthStoreManager.getKeyAudienceId());
|
|
61
|
+
},
|
|
62
|
+
deleteJwt(audienceId) {
|
|
63
|
+
deleteCookie(AuthStoreManager.getKeySession(audienceId));
|
|
64
|
+
},
|
|
65
|
+
getAudienceId() {
|
|
66
|
+
const cookieParsed = getCookie(AuthStoreManager.getKeyAudienceId());
|
|
67
|
+
const resultCookieParsed = audienceIdSchema.safeParse(cookieParsed);
|
|
68
|
+
if (resultCookieParsed.success) return resultCookieParsed.data.audienceId;
|
|
69
|
+
return null;
|
|
70
|
+
},
|
|
71
|
+
getJwt(audienceId) {
|
|
72
|
+
const cookieParsed = getCookie(AuthStoreManager.getKeySession(audienceId));
|
|
73
|
+
const resultCookieParsed = jwtSchema.safeParse(cookieParsed);
|
|
74
|
+
if (resultCookieParsed.success) return resultCookieParsed.data;
|
|
75
|
+
if (resultCookieParsed.error) {}
|
|
76
|
+
return null;
|
|
77
|
+
},
|
|
78
|
+
getKeyAudienceId() {
|
|
79
|
+
return `${KEY_AUDIENCE_ID}${AuthStoreManager.SUFFIX_KEY}`;
|
|
80
|
+
},
|
|
81
|
+
getKeySession(audienceId) {
|
|
82
|
+
return `${KEY_SESSION}${AuthStoreManager.SUFFIX_KEY}_${audienceId}`;
|
|
83
|
+
},
|
|
84
|
+
getUniqueHostnameString,
|
|
85
|
+
SUFFIX_KEY: DEFAULT_SUFFIX_KEY,
|
|
86
|
+
setAudienceId(audienceId) {
|
|
87
|
+
setCookie({ audienceId }, AuthStoreManager.getKeyAudienceId());
|
|
88
|
+
},
|
|
89
|
+
setJwt({ jwtInfo, setAudienceId = true }) {
|
|
90
|
+
const audienceId = jwtInfo.jwt?.audienceId;
|
|
91
|
+
setCookie(jwtInfo, AuthStoreManager.getKeySession(audienceId));
|
|
92
|
+
if (setAudienceId) this.setAudienceId(audienceId);
|
|
93
|
+
},
|
|
94
|
+
setSuffixKey(key) {
|
|
95
|
+
AuthStoreManager.SUFFIX_KEY = key ? `_${key}` : DEFAULT_SUFFIX_KEY;
|
|
96
|
+
},
|
|
97
|
+
typeGuardAudienceId(untypedObject) {
|
|
98
|
+
return audienceIdSchema.safeParse(untypedObject).success;
|
|
99
|
+
},
|
|
100
|
+
typeGuardJWT(untypedObject) {
|
|
101
|
+
return jwtSchema.safeParse(untypedObject).success;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
export { AuthStoreManager, setCookieConfig };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SESSION_HEADER_KEY, addAsyncHeaderInterceptor, createAdvancedClient, withAdditionalInterceptors, withProfile } from "@scaleway/sdk-client";
|
|
2
|
+
//#region src/useAuthScw/createClient.ts
|
|
3
|
+
const clientSingleton = {
|
|
4
|
+
IamV1Alpha1: void 0,
|
|
5
|
+
IamUnauthenticatedV1Alpha1: void 0,
|
|
6
|
+
clientSettings: void 0,
|
|
7
|
+
setAPIsAndSettings({ IamV1Alpha1, IamUnauthenticatedV1Alpha1, clientSettings }) {
|
|
8
|
+
this.IamV1Alpha1 = IamV1Alpha1;
|
|
9
|
+
this.IamUnauthenticatedV1Alpha1 = IamUnauthenticatedV1Alpha1;
|
|
10
|
+
this.clientSettings = clientSettings;
|
|
11
|
+
},
|
|
12
|
+
createClient({ getAsyncToken }) {
|
|
13
|
+
if (!this.IamV1Alpha1 || !this.IamUnauthenticatedV1Alpha1 || !this.clientSettings) throw "You must set apis and client settings to use package: auth-scw";
|
|
14
|
+
const client = createAdvancedClient(withProfile(this.clientSettings), withAdditionalInterceptors([...this.clientSettings.interceptors, { request: async ({ request }) => getAsyncToken ? addAsyncHeaderInterceptor(SESSION_HEADER_KEY, getAsyncToken)({ request }) : request }]));
|
|
15
|
+
const iamv1alpha1API = new this.IamV1Alpha1(client);
|
|
16
|
+
const iamUnauthenticatedV1Alpha1API = new this.IamUnauthenticatedV1Alpha1(client);
|
|
17
|
+
return {
|
|
18
|
+
deleteJWT: iamv1alpha1API.deleteJWT,
|
|
19
|
+
renewJWT: iamUnauthenticatedV1Alpha1API.renewJWT
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { clientSingleton };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AuthStoreManager } from "./authStoreManager.js";
|
|
2
|
+
import { clientSingleton } from "./createClient.js";
|
|
3
|
+
import { Errors } from "@scaleway/sdk-client";
|
|
4
|
+
//#region src/useAuthScw/helpers.ts
|
|
5
|
+
let refreshSessionPromise;
|
|
6
|
+
const refreshSession = ({ paramsRenewRequest, setJWT, onError }) => {
|
|
7
|
+
if (refreshSessionPromise !== void 0) return refreshSessionPromise;
|
|
8
|
+
const { renewJWT } = clientSingleton.createClient({});
|
|
9
|
+
refreshSessionPromise = renewJWT(paramsRenewRequest).then((jwtInfo) => {
|
|
10
|
+
setJWT(jwtInfo);
|
|
11
|
+
return jwtInfo;
|
|
12
|
+
}).catch((error) => {
|
|
13
|
+
if (onError) onError({
|
|
14
|
+
error,
|
|
15
|
+
extra: {
|
|
16
|
+
iframe: globalThis.self !== globalThis.top,
|
|
17
|
+
jti: paramsRenewRequest.jti,
|
|
18
|
+
message: error instanceof Errors.ScalewayError ? error.rawMessage : void 0,
|
|
19
|
+
renewToken: paramsRenewRequest.renewToken
|
|
20
|
+
},
|
|
21
|
+
title: "Renew token failed"
|
|
22
|
+
});
|
|
23
|
+
}).finally(() => {
|
|
24
|
+
refreshSessionPromise = void 0;
|
|
25
|
+
});
|
|
26
|
+
return refreshSessionPromise;
|
|
27
|
+
};
|
|
28
|
+
const decodeToken = (encodedToken) => {
|
|
29
|
+
try {
|
|
30
|
+
const decodedTokenRaw = JSON.parse(atob(encodedToken));
|
|
31
|
+
if (AuthStoreManager.typeGuardJWT(decodedTokenRaw)) return decodedTokenRaw;
|
|
32
|
+
return null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const encodeToken = (jwt) => btoa(JSON.stringify(jwt));
|
|
38
|
+
const getCookieJWT = (audienceId) => AuthStoreManager.getJwt(audienceId);
|
|
39
|
+
//#endregion
|
|
40
|
+
export { decodeToken, encodeToken, getCookieJWT, refreshSession };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { AuthScwProvider, useAuthScw } from "./AuthScwProvider.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { subMinutes, subSeconds } from "date-fns";
|
|
2
|
+
//#region src/useAuthScw/proxyJwt.ts
|
|
3
|
+
const defaultUpdateValue = {
|
|
4
|
+
shouldOutdated: false,
|
|
5
|
+
subMinuteOfExpires: 59,
|
|
6
|
+
subSecondesOfExpires: 20
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* This function is only use for Development purpose only !!!
|
|
10
|
+
* internal only
|
|
11
|
+
* @param jwtInfoParam
|
|
12
|
+
* @param shouldOutdatedJWT
|
|
13
|
+
* @returns jwt
|
|
14
|
+
*/
|
|
15
|
+
const proxyJwt = (jwtInfoParam, updateJwt = defaultUpdateValue) => {
|
|
16
|
+
if (updateJwt.shouldOutdated) {
|
|
17
|
+
const oldJwt = {
|
|
18
|
+
...jwtInfoParam.jwt,
|
|
19
|
+
expiresAt: jwtInfoParam.jwt?.expiresAt ? subSeconds(subMinutes(new Date(jwtInfoParam.jwt.expiresAt), updateJwt.subMinuteOfExpires), updateJwt.subSecondesOfExpires) : void 0
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
...jwtInfoParam,
|
|
23
|
+
jwt: oldJwt
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return jwtInfoParam;
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { proxyJwt };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Iamv1alpha1 } from "./types/iam/index.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/zodSchemas.d.ts
|
|
5
|
+
type TransformJWT = Omit<Iamv1alpha1.JWT, 'createdAt' | 'updatedAt' | 'expiresAt'> & {
|
|
6
|
+
createdAt?: string | Date;
|
|
7
|
+
updatedAt?: string | Date;
|
|
8
|
+
expiresAt?: string | Date;
|
|
9
|
+
};
|
|
10
|
+
type ValidateJWTSchema = {
|
|
11
|
+
jwt?: TransformJWT;
|
|
12
|
+
token: string;
|
|
13
|
+
renewToken: string;
|
|
14
|
+
};
|
|
15
|
+
declare const jwtSchema: z.ZodType<ValidateJWTSchema>;
|
|
16
|
+
declare const audienceIdSchema: z.ZodObject<{
|
|
17
|
+
audienceId: z.ZodString;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { audienceIdSchema, jwtSchema };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/zodSchemas.ts
|
|
3
|
+
const jwtSchema = z.object({
|
|
4
|
+
jwt: z.object({
|
|
5
|
+
audienceId: z.string().min(1),
|
|
6
|
+
createdAt: z.union([z.string().min(1), z.date()]).optional(),
|
|
7
|
+
expiresAt: z.union([z.string().min(1), z.date()]).optional(),
|
|
8
|
+
ip: z.string().min(1),
|
|
9
|
+
issuerId: z.string().min(1),
|
|
10
|
+
jti: z.string().min(1),
|
|
11
|
+
updatedAt: z.union([z.string().min(1), z.date()]).optional(),
|
|
12
|
+
userAgent: z.string().min(1)
|
|
13
|
+
}),
|
|
14
|
+
renewToken: z.string().min(1),
|
|
15
|
+
token: z.string().min(1)
|
|
16
|
+
});
|
|
17
|
+
const audienceIdSchema = z.object({ audienceId: z.string().min(1) });
|
|
18
|
+
//#endregion
|
|
19
|
+
export { audienceIdSchema, jwtSchema };
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scaleway/auth-scw",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Auth Scaleway",
|
|
5
|
+
"homepage": "https://github.com/scaleway/scaleway-lib/tree/main/packages/auth-scw",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/scaleway/scaleway-lib/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/scaleway/scaleway-lib.git",
|
|
13
|
+
"directory": "packages/auth-scw"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/*"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./dist/index.js",
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@scaleway/sdk-client": "2.4.1",
|
|
30
|
+
"@scaleway/sdk-iam": "2.9.4",
|
|
31
|
+
"@types/react": "19.2.17",
|
|
32
|
+
"cookie": "2.0.0",
|
|
33
|
+
"date-fns": "4.4.0",
|
|
34
|
+
"react": "19.2.7",
|
|
35
|
+
"zod": "4.4.3"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@scaleway/sdk-client": "2.4.1",
|
|
39
|
+
"@scaleway/sdk-iam": "2.9.4",
|
|
40
|
+
"cookie": "2.0.0",
|
|
41
|
+
"date-fns": "4.4.0",
|
|
42
|
+
"react": "19.2.7",
|
|
43
|
+
"zod": "4.4.3"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"build": "tsdown",
|
|
48
|
+
"test:unit": "vitest --run",
|
|
49
|
+
"test:unit:coverage": "pnpm test:unit --coverage"
|
|
50
|
+
}
|
|
51
|
+
}
|