@cemiar/auth-sdk 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/README.md +122 -0
- package/dist/browser/CemiarAuthClient.d.ts +30 -0
- package/dist/browser/CemiarAuthClient.js +171 -0
- package/dist/browser/Storage.d.ts +4 -0
- package/dist/browser/Storage.js +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/server/ServerHandler.d.ts +8 -0
- package/dist/server/ServerHandler.js +59 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/shared/Types.d.ts +81 -0
- package/dist/shared/Types.js +1 -0
- package/dist/shared/Utils.d.ts +2 -0
- package/dist/shared/Utils.js +7 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# @cemiar/auth-sdk
|
|
2
|
+
|
|
3
|
+
Reusable helpers for integrating Cemiar Auth in web front-ends and Node.js APIs.
|
|
4
|
+
|
|
5
|
+
This package bundles:
|
|
6
|
+
|
|
7
|
+
- A browser-friendly Cemiar Auth client with email codes, Microsoft login support, token storage, refresh and logout helpers
|
|
8
|
+
- Axios utilities to attach automatic Authorization headers and refresh tokens on 401 responses
|
|
9
|
+
- A Hapi JWT strategy helper that fetches Cemiar Auth signing keys from JWKS and validates incoming requests
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @cemiar/auth-sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
When working inside this monorepo, you can add it via a file reference:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @cemiar/auth-sdk@file:../packages/cemiar-auth-sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Make sure the host project also has the required peer dependencies installed (`@hapi/hapi`, `hapi-auth-jwt2`).
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Front-end (Vue/React/etc.)
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createCemiarAuthClient } from "@cemiar/auth-sdk";
|
|
31
|
+
|
|
32
|
+
const authClient = createCemiarAuthClient({
|
|
33
|
+
baseUrl: import.meta.env.VITE_CEMIAR_AUTH_URL!,
|
|
34
|
+
tenantId: import.meta.env.VITE_CEMIAR_TENANT_ID!,
|
|
35
|
+
auds: (import.meta.env.VITE_CEMIAR_AUDS || "").split(","),
|
|
36
|
+
redirectUrl: `${window.location.origin}/auth/microsoft/callback`,
|
|
37
|
+
logoutRedirectUrl: `${window.location.origin}/login`
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Trigger Microsoft login
|
|
41
|
+
window.location.href = authClient.getMicrosoftLoginUrl();
|
|
42
|
+
|
|
43
|
+
// Email code flow
|
|
44
|
+
await authClient.sendEmailCode("user@example.com");
|
|
45
|
+
const { accessToken } = await authClient.verifyEmailCode("user@example.com", "123456");
|
|
46
|
+
|
|
47
|
+
// Axios client with automatic Authorization header and refresh-on-401
|
|
48
|
+
const api = authClient.createApiClient({ baseURL: import.meta.env.VITE_API_BASE });
|
|
49
|
+
const jobs = await api.get("/api/jobs");
|
|
50
|
+
|
|
51
|
+
// Manual interceptor attachment if you already have an axios instance
|
|
52
|
+
authClient.attachInterceptors(existingAxiosInstance);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Token storage hooks
|
|
56
|
+
|
|
57
|
+
By default the client stores tokens in `localStorage`. Override storage to customize behaviour:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { createCemiarAuthClient, TokenStorage } from "@cemiar/auth-sdk";
|
|
61
|
+
|
|
62
|
+
const storage: TokenStorage = {
|
|
63
|
+
getToken: () => cookies.get("cemiar-access"),
|
|
64
|
+
setToken: token => {
|
|
65
|
+
token ? cookies.set("cemiar-access", token) : cookies.remove("cemiar-access");
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const authClient = createCemiarAuthClient({
|
|
70
|
+
baseUrl: import.meta.env.VITE_CEMIAR_AUTH_URL!,
|
|
71
|
+
tenantId: import.meta.env.VITE_CEMIAR_TENANT_ID!,
|
|
72
|
+
auds: ["cemiar-jobs"],
|
|
73
|
+
storage
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Backend (Hapi)
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { registerCemiarAuthHapi } from "@cemiar/auth-sdk";
|
|
81
|
+
|
|
82
|
+
await registerCemiarAuthHapi(server, {
|
|
83
|
+
authUrl: process.env.CEMIAR_AUTH_URL!,
|
|
84
|
+
strategyName: "jwt",
|
|
85
|
+
validate: async decoded => ({
|
|
86
|
+
isValid: true,
|
|
87
|
+
credentials: decoded.payload
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The helper automatically fetches signing keys from Cemiar Auth JWKS endpoint and keeps them cached.
|
|
93
|
+
|
|
94
|
+
To customise the cache and algorithms:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
await registerCemiarAuthHapi(server, {
|
|
98
|
+
authUrl: process.env.CEMIAR_AUTH_URL!,
|
|
99
|
+
algorithms: ["RS256"],
|
|
100
|
+
cacheMaxAgeMs: 12 * 60 * 60 * 1000
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Environment Variables
|
|
105
|
+
|
|
106
|
+
| Variable | Description |
|
|
107
|
+
| -------------------------- | ------------------------------------------------------------------ |
|
|
108
|
+
| `VITE_CEMIAR_AUTH_URL` | Base Cemiar Auth URL (e.g. `https://cemiar-auth.example.com/auth`) |
|
|
109
|
+
| `VITE_CEMIAR_TENANT_ID` | Tenant identifier provided by Cemiar Auth |
|
|
110
|
+
| `VITE_CEMIAR_AUDS` | Comma separated list of audiences |
|
|
111
|
+
| `VITE_CEMIAR_REDIRECT_URL` | Optional override for Microsoft redirect |
|
|
112
|
+
| `CEMIAR_AUTH_URL` | Backend URL for Cemiar Auth (no Vite prefix) |
|
|
113
|
+
|
|
114
|
+
### Building
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
cd packages/cemiar-auth-sdk
|
|
118
|
+
npm install
|
|
119
|
+
npm run build
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Output will be generated under `dist/`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AxiosInstance } from "axios";
|
|
2
|
+
import type { AuthTokens, CemiarAuthClientConfig, CemiarAuthClientInstance, CreateApiClientOptions, EmailCodeResponse, LogoutOptions, VerifyEmailResult } from "../shared/Types.js";
|
|
3
|
+
export declare class CemiarAuthClient implements CemiarAuthClientInstance {
|
|
4
|
+
private readonly baseUrl;
|
|
5
|
+
private readonly tenantId;
|
|
6
|
+
private readonly auds;
|
|
7
|
+
private readonly redirectUrl?;
|
|
8
|
+
private storage;
|
|
9
|
+
private readonly onTokenChange?;
|
|
10
|
+
private readonly onAuthFailure?;
|
|
11
|
+
private readonly logoutRedirectUrl?;
|
|
12
|
+
private refreshPromise;
|
|
13
|
+
constructor(config: CemiarAuthClientConfig);
|
|
14
|
+
getAccessToken(): string | null;
|
|
15
|
+
setAccessToken(token: string | null): void;
|
|
16
|
+
private authPost;
|
|
17
|
+
sendEmailCode(email: string): Promise<EmailCodeResponse>;
|
|
18
|
+
verifyEmailCode(email: string, code: string): Promise<VerifyEmailResult>;
|
|
19
|
+
getMicrosoftLoginUrl(extraParams?: Record<string, string>): string;
|
|
20
|
+
exchangeMicrosoftCode(code: string): Promise<AuthTokens>;
|
|
21
|
+
refreshToken(): Promise<AuthTokens>;
|
|
22
|
+
logout(options?: LogoutOptions): Promise<void>;
|
|
23
|
+
createApiClient(options: CreateApiClientOptions): AxiosInstance;
|
|
24
|
+
attachInterceptors(instance: AxiosInstance): AxiosInstance;
|
|
25
|
+
private addAuthHeader;
|
|
26
|
+
private handleResponseError;
|
|
27
|
+
private queueTokenRefresh;
|
|
28
|
+
private handleAuthFailure;
|
|
29
|
+
}
|
|
30
|
+
export declare function createCemiarAuthClient(config: CemiarAuthClientConfig): CemiarAuthClientInstance;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { createDefaultTokenStorage } from "./Storage.js";
|
|
3
|
+
import { normalizeBaseUrl, isBrowser } from "../shared/Utils.js";
|
|
4
|
+
function parseAuds(auds) {
|
|
5
|
+
return auds.map(a => a.trim()).filter(Boolean);
|
|
6
|
+
}
|
|
7
|
+
function extractAccessToken(data) {
|
|
8
|
+
var _a;
|
|
9
|
+
const token = (_a = data.accessToken) !== null && _a !== void 0 ? _a : data.token;
|
|
10
|
+
if (typeof token === "string" && token.length > 0) {
|
|
11
|
+
return token;
|
|
12
|
+
}
|
|
13
|
+
throw new Error("Cemiar Auth response missing access token");
|
|
14
|
+
}
|
|
15
|
+
export class CemiarAuthClient {
|
|
16
|
+
constructor(config) {
|
|
17
|
+
var _a, _b, _c;
|
|
18
|
+
this.storage = createDefaultTokenStorage();
|
|
19
|
+
this.refreshPromise = null;
|
|
20
|
+
this.baseUrl = normalizeBaseUrl(config.baseUrl || "http://localhost:3000/auth");
|
|
21
|
+
this.tenantId = config.tenantId;
|
|
22
|
+
this.auds = parseAuds(config.auds);
|
|
23
|
+
this.redirectUrl =
|
|
24
|
+
(_a = config.redirectUrl) !== null && _a !== void 0 ? _a : (isBrowser ? `${window.location.origin}/auth/microsoft/callback` : undefined);
|
|
25
|
+
this.onTokenChange = config.onTokenChange;
|
|
26
|
+
this.onAuthFailure = config.onAuthFailure;
|
|
27
|
+
this.logoutRedirectUrl =
|
|
28
|
+
(_b = config.logoutRedirectUrl) !== null && _b !== void 0 ? _b : (isBrowser ? `${window.location.origin}/login` : undefined);
|
|
29
|
+
this.storage = (_c = config.storage) !== null && _c !== void 0 ? _c : createDefaultTokenStorage();
|
|
30
|
+
}
|
|
31
|
+
getAccessToken() {
|
|
32
|
+
return this.storage.getToken();
|
|
33
|
+
}
|
|
34
|
+
setAccessToken(token) {
|
|
35
|
+
var _a;
|
|
36
|
+
this.storage.setToken(token);
|
|
37
|
+
(_a = this.onTokenChange) === null || _a === void 0 ? void 0 : _a.call(this, token);
|
|
38
|
+
}
|
|
39
|
+
async authPost(path, body, withCredentials = true) {
|
|
40
|
+
const url = `${this.baseUrl}${path}`;
|
|
41
|
+
const response = await axios.post(url, body, {
|
|
42
|
+
withCredentials
|
|
43
|
+
});
|
|
44
|
+
return response.data;
|
|
45
|
+
}
|
|
46
|
+
async sendEmailCode(email) {
|
|
47
|
+
return this.authPost("/emailCodes/generate", { email });
|
|
48
|
+
}
|
|
49
|
+
async verifyEmailCode(email, code) {
|
|
50
|
+
const payload = {
|
|
51
|
+
email,
|
|
52
|
+
code,
|
|
53
|
+
tenantId: this.tenantId,
|
|
54
|
+
auds: this.auds
|
|
55
|
+
};
|
|
56
|
+
const data = await this.authPost("/emailCodes/validate", payload);
|
|
57
|
+
const accessToken = extractAccessToken(data);
|
|
58
|
+
this.setAccessToken(accessToken);
|
|
59
|
+
return {
|
|
60
|
+
accessToken,
|
|
61
|
+
authenticated: Boolean(data.authenticated),
|
|
62
|
+
blacklisted: Boolean(data.blacklisted),
|
|
63
|
+
raw: data
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
getMicrosoftLoginUrl(extraParams = {}) {
|
|
67
|
+
const params = new URLSearchParams({
|
|
68
|
+
tenantId: this.tenantId,
|
|
69
|
+
auds: this.auds.join(","),
|
|
70
|
+
...(this.redirectUrl ? { redirectUrl: this.redirectUrl } : {})
|
|
71
|
+
});
|
|
72
|
+
for (const [key, value] of Object.entries(extraParams)) {
|
|
73
|
+
params.set(key, value);
|
|
74
|
+
}
|
|
75
|
+
return `${this.baseUrl}/microsoft?${params.toString()}`;
|
|
76
|
+
}
|
|
77
|
+
async exchangeMicrosoftCode(code) {
|
|
78
|
+
const payload = {
|
|
79
|
+
code,
|
|
80
|
+
...(this.redirectUrl ? { redirectUrl: this.redirectUrl } : {})
|
|
81
|
+
};
|
|
82
|
+
const data = await this.authPost("/microsoft/callback", payload);
|
|
83
|
+
const accessToken = extractAccessToken(data);
|
|
84
|
+
this.setAccessToken(accessToken);
|
|
85
|
+
return { accessToken };
|
|
86
|
+
}
|
|
87
|
+
async refreshToken() {
|
|
88
|
+
const data = await this.authPost("/refresh", {}, true);
|
|
89
|
+
const accessToken = extractAccessToken(data);
|
|
90
|
+
this.setAccessToken(accessToken);
|
|
91
|
+
return { accessToken };
|
|
92
|
+
}
|
|
93
|
+
async logout(options = {}) {
|
|
94
|
+
if (options.clearToken !== false) {
|
|
95
|
+
this.setAccessToken(null);
|
|
96
|
+
}
|
|
97
|
+
const redirectUrl = options.redirectUrl || this.logoutRedirectUrl;
|
|
98
|
+
const logoutUrl = redirectUrl
|
|
99
|
+
? `${this.baseUrl}/microsoft/logout?redirectUrl=${encodeURIComponent(redirectUrl)}`
|
|
100
|
+
: `${this.baseUrl}/logout`;
|
|
101
|
+
try {
|
|
102
|
+
if (options.performRedirect === false || !isBrowser) {
|
|
103
|
+
await axios.post(`${this.baseUrl}/logout`, {}, { withCredentials: true });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
window.location.href = logoutUrl;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// ignore network errors during logout
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
createApiClient(options) {
|
|
113
|
+
var _a;
|
|
114
|
+
const instance = axios.create({
|
|
115
|
+
baseURL: options.baseURL,
|
|
116
|
+
withCredentials: true,
|
|
117
|
+
...((_a = options.axiosConfig) !== null && _a !== void 0 ? _a : {})
|
|
118
|
+
});
|
|
119
|
+
return this.attachInterceptors(instance);
|
|
120
|
+
}
|
|
121
|
+
attachInterceptors(instance) {
|
|
122
|
+
instance.interceptors.request.use(config => this.addAuthHeader(config));
|
|
123
|
+
instance.interceptors.response.use(response => response, error => this.handleResponseError(error, instance));
|
|
124
|
+
return instance;
|
|
125
|
+
}
|
|
126
|
+
addAuthHeader(config) {
|
|
127
|
+
const token = this.getAccessToken();
|
|
128
|
+
if (token && config.headers) {
|
|
129
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
130
|
+
}
|
|
131
|
+
return config;
|
|
132
|
+
}
|
|
133
|
+
async handleResponseError(error, instance) {
|
|
134
|
+
var _a;
|
|
135
|
+
const status = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
136
|
+
const originalRequest = error.config;
|
|
137
|
+
if (status !== 401 || !originalRequest || originalRequest._retry) {
|
|
138
|
+
return Promise.reject(error);
|
|
139
|
+
}
|
|
140
|
+
originalRequest._retry = true;
|
|
141
|
+
try {
|
|
142
|
+
const newToken = await this.queueTokenRefresh();
|
|
143
|
+
if (originalRequest.headers) {
|
|
144
|
+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
|
145
|
+
}
|
|
146
|
+
return instance(originalRequest);
|
|
147
|
+
}
|
|
148
|
+
catch (refreshError) {
|
|
149
|
+
this.handleAuthFailure();
|
|
150
|
+
return Promise.reject(refreshError);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async queueTokenRefresh() {
|
|
154
|
+
if (!this.refreshPromise) {
|
|
155
|
+
this.refreshPromise = this.refreshToken()
|
|
156
|
+
.then(tokens => tokens.accessToken)
|
|
157
|
+
.finally(() => {
|
|
158
|
+
this.refreshPromise = null;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return this.refreshPromise;
|
|
162
|
+
}
|
|
163
|
+
handleAuthFailure() {
|
|
164
|
+
var _a;
|
|
165
|
+
this.setAccessToken(null);
|
|
166
|
+
(_a = this.onAuthFailure) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function createCemiarAuthClient(config) {
|
|
170
|
+
return new CemiarAuthClient(config);
|
|
171
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TokenStorage } from "../shared/Types.js";
|
|
2
|
+
export declare function createMemoryStorage(initialValue?: string | null): TokenStorage;
|
|
3
|
+
export declare function createLocalStorageWrapper(key?: string): TokenStorage;
|
|
4
|
+
export declare function createDefaultTokenStorage(): TokenStorage;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isBrowser } from "../shared/Utils.js";
|
|
2
|
+
export function createMemoryStorage(initialValue = null) {
|
|
3
|
+
let token = initialValue;
|
|
4
|
+
return {
|
|
5
|
+
getToken: () => token,
|
|
6
|
+
setToken: value => {
|
|
7
|
+
token = value;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function createLocalStorageWrapper(key = "accessToken") {
|
|
12
|
+
if (!isBrowser || !("localStorage" in globalThis)) {
|
|
13
|
+
return createMemoryStorage(null);
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
getToken: () => {
|
|
17
|
+
try {
|
|
18
|
+
return window.localStorage.getItem(key);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.warn("Failed to read token from localStorage", error);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
setToken: value => {
|
|
26
|
+
try {
|
|
27
|
+
if (value) {
|
|
28
|
+
window.localStorage.setItem(key, value);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
window.localStorage.removeItem(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.warn("Failed to write token to localStorage", error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function createDefaultTokenStorage() {
|
|
41
|
+
return createLocalStorageWrapper();
|
|
42
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HapiJwtStrategyOptions, JwtParts } from "../shared/Types.js";
|
|
2
|
+
import type { Server } from "@hapi/hapi";
|
|
3
|
+
declare function createSigningKeyResolver(jwksUri: string, cacheMaxAgeMs: number): {
|
|
4
|
+
getKey: (decoded: JwtParts) => Promise<string>;
|
|
5
|
+
};
|
|
6
|
+
export declare function registerCemiarAuthHapi(server: Server, options: HapiJwtStrategyOptions): Promise<void>;
|
|
7
|
+
export { createSigningKeyResolver };
|
|
8
|
+
export type { JwtValidateFn, JwtValidateResult, JwtParts } from "../shared/Types.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import jwksClient from "jwks-rsa";
|
|
2
|
+
import { normalizeBaseUrl } from "../shared/Utils.js";
|
|
3
|
+
const DEFAULT_ALGORITHMS = ["RS256"];
|
|
4
|
+
const DEFAULT_CACHE_MAX_AGE = 24 * 60 * 60 * 1000; // 24 hours
|
|
5
|
+
const defaultValidate = async (decoded) => ({
|
|
6
|
+
isValid: true,
|
|
7
|
+
credentials: decoded.payload
|
|
8
|
+
});
|
|
9
|
+
function createSigningKeyResolver(jwksUri, cacheMaxAgeMs) {
|
|
10
|
+
const client = jwksClient({
|
|
11
|
+
jwksUri,
|
|
12
|
+
cache: true,
|
|
13
|
+
cacheMaxEntries: 5,
|
|
14
|
+
cacheMaxAge: cacheMaxAgeMs
|
|
15
|
+
});
|
|
16
|
+
const getKey = async (decoded) => {
|
|
17
|
+
var _a;
|
|
18
|
+
const kid = (_a = decoded.header) === null || _a === void 0 ? void 0 : _a.kid;
|
|
19
|
+
if (!kid) {
|
|
20
|
+
throw new Error("JWT header is missing 'kid' claim");
|
|
21
|
+
}
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
client.getSigningKey(kid, (error, key) => {
|
|
24
|
+
if (error) {
|
|
25
|
+
return reject(error);
|
|
26
|
+
}
|
|
27
|
+
if (!key) {
|
|
28
|
+
return reject(new Error(`Unable to resolve signing key for kid ${kid}`));
|
|
29
|
+
}
|
|
30
|
+
resolve(key.getPublicKey());
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
return { getKey };
|
|
35
|
+
}
|
|
36
|
+
export async function registerCemiarAuthHapi(server, options) {
|
|
37
|
+
var _a;
|
|
38
|
+
const { authUrl, strategyName = "cemiar-jwt", algorithms = DEFAULT_ALGORITHMS, cacheMaxAgeMs = DEFAULT_CACHE_MAX_AGE, validate = defaultValidate } = options;
|
|
39
|
+
const jwksUri = `${normalizeBaseUrl(authUrl)}/.well-known/jwks.json`;
|
|
40
|
+
const { getKey } = createSigningKeyResolver(jwksUri, cacheMaxAgeMs);
|
|
41
|
+
const pluginModule = await import("hapi-auth-jwt2");
|
|
42
|
+
const plugin = ((_a = pluginModule.default) !== null && _a !== void 0 ? _a : pluginModule);
|
|
43
|
+
await server.register(plugin);
|
|
44
|
+
server.auth.strategy(strategyName, "jwt", {
|
|
45
|
+
key: async (decoded) => {
|
|
46
|
+
const key = await getKey(decoded);
|
|
47
|
+
return { key };
|
|
48
|
+
},
|
|
49
|
+
validate: async (decoded, request, h) => {
|
|
50
|
+
return validate(decoded, request, h);
|
|
51
|
+
},
|
|
52
|
+
verifyOptions: {
|
|
53
|
+
algorithms
|
|
54
|
+
},
|
|
55
|
+
complete: true
|
|
56
|
+
});
|
|
57
|
+
server.auth.default(strategyName);
|
|
58
|
+
}
|
|
59
|
+
export { createSigningKeyResolver };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerCemiarAuthHapi, createSigningKeyResolver } from "./ServerHandler.js";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { AxiosInstance, AxiosRequestConfig } from "axios";
|
|
2
|
+
import type { Request, ResponseToolkit } from "@hapi/hapi";
|
|
3
|
+
export interface TokenStorage {
|
|
4
|
+
getToken(): string | null;
|
|
5
|
+
setToken(token: string | null): void;
|
|
6
|
+
}
|
|
7
|
+
export interface CemiarAuthClientConfig {
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
tenantId: string;
|
|
10
|
+
auds: string[];
|
|
11
|
+
redirectUrl?: string;
|
|
12
|
+
storage?: TokenStorage;
|
|
13
|
+
onTokenChange?: (token: string | null) => void;
|
|
14
|
+
onAuthFailure?: () => void;
|
|
15
|
+
logoutRedirectUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AuthTokens {
|
|
18
|
+
accessToken: string;
|
|
19
|
+
}
|
|
20
|
+
export interface EmailCodeResponse {
|
|
21
|
+
success: boolean;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface EmailVerifyResponse {
|
|
25
|
+
token: string;
|
|
26
|
+
authenticated: boolean;
|
|
27
|
+
blacklisted: boolean;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
export interface VerifyEmailResult extends AuthTokens {
|
|
31
|
+
authenticated: boolean;
|
|
32
|
+
blacklisted: boolean;
|
|
33
|
+
raw: EmailVerifyResponse;
|
|
34
|
+
}
|
|
35
|
+
export interface RefreshResponse {
|
|
36
|
+
accessToken?: string;
|
|
37
|
+
token?: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
export interface CreateApiClientOptions {
|
|
41
|
+
baseURL: string;
|
|
42
|
+
axiosConfig?: AxiosRequestConfig;
|
|
43
|
+
}
|
|
44
|
+
export interface LogoutOptions {
|
|
45
|
+
redirectUrl?: string;
|
|
46
|
+
performRedirect?: boolean;
|
|
47
|
+
clearToken?: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface JwtHeader {
|
|
50
|
+
kid: string;
|
|
51
|
+
alg: string;
|
|
52
|
+
}
|
|
53
|
+
export interface JwtParts {
|
|
54
|
+
header: JwtHeader;
|
|
55
|
+
payload: unknown;
|
|
56
|
+
signature?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface JwtValidateResult {
|
|
59
|
+
isValid: boolean;
|
|
60
|
+
credentials?: unknown;
|
|
61
|
+
}
|
|
62
|
+
export type JwtValidateFn = (decoded: JwtParts, request: Request, h: ResponseToolkit) => Promise<JwtValidateResult> | JwtValidateResult;
|
|
63
|
+
export interface HapiJwtStrategyOptions {
|
|
64
|
+
authUrl: string;
|
|
65
|
+
strategyName?: string;
|
|
66
|
+
algorithms?: string[];
|
|
67
|
+
cacheMaxAgeMs?: number;
|
|
68
|
+
validate?: JwtValidateFn;
|
|
69
|
+
}
|
|
70
|
+
export interface CemiarAuthClientInstance {
|
|
71
|
+
getAccessToken(): string | null;
|
|
72
|
+
setAccessToken(token: string | null): void;
|
|
73
|
+
getMicrosoftLoginUrl(params?: Record<string, string>): string;
|
|
74
|
+
sendEmailCode(email: string): Promise<EmailCodeResponse>;
|
|
75
|
+
verifyEmailCode(email: string, code: string): Promise<VerifyEmailResult>;
|
|
76
|
+
exchangeMicrosoftCode(code: string): Promise<AuthTokens>;
|
|
77
|
+
refreshToken(): Promise<AuthTokens>;
|
|
78
|
+
logout(options?: LogoutOptions): Promise<void>;
|
|
79
|
+
createApiClient(options: CreateApiClientOptions): AxiosInstance;
|
|
80
|
+
attachInterceptors(instance: AxiosInstance): AxiosInstance;
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cemiar/auth-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cemiar Auth integration helpers for web apps and APIs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./server": {
|
|
14
|
+
"import": "./dist/server/index.js",
|
|
15
|
+
"types": "./dist/server/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"clean": "rimraf dist"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"cemiar",
|
|
28
|
+
"auth",
|
|
29
|
+
"jwt",
|
|
30
|
+
"hapi",
|
|
31
|
+
"axios"
|
|
32
|
+
],
|
|
33
|
+
"author": "Cemiar",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"axios": "^1.7.0",
|
|
37
|
+
"jwks-rsa": "^3.1.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@hapi/hapi": "^21.3.0",
|
|
41
|
+
"hapi-auth-jwt2": "^10.5.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@hapi/hapi": "^21.3.0",
|
|
45
|
+
"@types/node": "^20.11.30",
|
|
46
|
+
"hapi-auth-jwt2": "^10.5.0",
|
|
47
|
+
"rimraf": "^5.0.5",
|
|
48
|
+
"typescript": "^5.3.3"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|