@fjordid/client 0.1.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 +158 -0
- package/dist/FjordAuth.d.ts +90 -0
- package/dist/FjordAuth.d.ts.map +1 -0
- package/dist/FjordAuth.js +295 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @fjordid/client
|
|
2
|
+
|
|
3
|
+
Lightweight JavaScript/TypeScript client for [FjordID](https://fjordid.eu) authentication. Wraps Keycloak OIDC with a simple, developer-friendly API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @fjordid/client
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @fjordid/client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { FjordAuth } from "@fjordid/client";
|
|
17
|
+
|
|
18
|
+
const auth = new FjordAuth({
|
|
19
|
+
domain: "your-app.fjordid.eu",
|
|
20
|
+
clientId: "your-client-id",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await auth.init();
|
|
24
|
+
auth.login();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Initialize
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const auth = new FjordAuth({
|
|
33
|
+
domain: "your-app.fjordid.eu",
|
|
34
|
+
clientId: "your-client-id",
|
|
35
|
+
// Optional overrides:
|
|
36
|
+
// authUrl: 'https://auth.fjordid.eu',
|
|
37
|
+
// realm: 'fjordid',
|
|
38
|
+
// redirectUri: window.location.origin,
|
|
39
|
+
// silentSso: true,
|
|
40
|
+
// initTimeout: 10000,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const isLoggedIn = await auth.init();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Login / Register / Logout
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
auth.login(); // Redirect to login page
|
|
50
|
+
auth.register(); // Redirect to registration page
|
|
51
|
+
auth.logout(); // Log out and redirect home
|
|
52
|
+
|
|
53
|
+
// Custom redirect after login
|
|
54
|
+
auth.login("/dashboard");
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### User Info
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
if (auth.authenticated) {
|
|
61
|
+
console.log(auth.user?.email);
|
|
62
|
+
console.log(auth.user?.name);
|
|
63
|
+
console.log(auth.user?.id);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Access Tokens
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Get a valid token (auto-refreshes if expiring soon)
|
|
71
|
+
const token = await auth.getToken();
|
|
72
|
+
|
|
73
|
+
// Use in API calls
|
|
74
|
+
const res = await fetch("/api/data", {
|
|
75
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Events
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const unsub = auth.on("onAuthSuccess", () => {
|
|
83
|
+
console.log("Logged in:", auth.user?.email);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
auth.on("onAuthLogout", () => {
|
|
87
|
+
console.log("Logged out");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Unsubscribe
|
|
91
|
+
unsub();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API Reference
|
|
95
|
+
|
|
96
|
+
### `new FjordAuth(options)`
|
|
97
|
+
|
|
98
|
+
| Option | Type | Default | Description |
|
|
99
|
+
| ----------------------- | --------- | ------------------------- | -------------------------------- |
|
|
100
|
+
| `domain` | `string` | **required** | Your FjordID domain |
|
|
101
|
+
| `clientId` | `string` | **required** | OIDC client ID |
|
|
102
|
+
| `authUrl` | `string` | `https://auth.fjordid.eu` | Keycloak server URL |
|
|
103
|
+
| `realm` | `string` | `fjordid` | Keycloak realm |
|
|
104
|
+
| `redirectUri` | `string` | `window.location.origin` | Post-login redirect |
|
|
105
|
+
| `postLogoutRedirectUri` | `string` | `window.location.origin` | Post-logout redirect |
|
|
106
|
+
| `silentSso` | `boolean` | `true` | Enable silent SSO check |
|
|
107
|
+
| `initTimeout` | `number` | `10000` | Init timeout (ms) |
|
|
108
|
+
| `tokenRefreshBuffer` | `number` | `30` | Seconds before expiry to refresh |
|
|
109
|
+
|
|
110
|
+
### Properties
|
|
111
|
+
|
|
112
|
+
| Property | Type | Description |
|
|
113
|
+
| --------------- | --------------------- | ----------------------------- |
|
|
114
|
+
| `authenticated` | `boolean` | Whether the user is logged in |
|
|
115
|
+
| `user` | `FjordUser \| null` | Current user profile |
|
|
116
|
+
| `token` | `string \| undefined` | Raw access token |
|
|
117
|
+
| `idToken` | `string \| undefined` | Raw ID token |
|
|
118
|
+
|
|
119
|
+
### Methods
|
|
120
|
+
|
|
121
|
+
| Method | Returns | Description |
|
|
122
|
+
| ------------------------ | ------------------------------ | --------------------------- |
|
|
123
|
+
| `init()` | `Promise<boolean>` | Initialize auth (call once) |
|
|
124
|
+
| `login(redirectUri?)` | `void` | Redirect to login |
|
|
125
|
+
| `register(redirectUri?)` | `void` | Redirect to registration |
|
|
126
|
+
| `logout(redirectUri?)` | `void` | Log out |
|
|
127
|
+
| `getToken()` | `Promise<string \| undefined>` | Get valid access token |
|
|
128
|
+
| `refreshToken()` | `Promise<string \| undefined>` | Force token refresh |
|
|
129
|
+
| `on(event, callback)` | `() => void` | Subscribe to events |
|
|
130
|
+
|
|
131
|
+
### Events
|
|
132
|
+
|
|
133
|
+
| Event | When |
|
|
134
|
+
| ---------------- | -------------------------------- |
|
|
135
|
+
| `onReady` | Keycloak adapter ready |
|
|
136
|
+
| `onAuthSuccess` | Login succeeded |
|
|
137
|
+
| `onAuthLogout` | User logged out |
|
|
138
|
+
| `onAuthError` | Auth error occurred |
|
|
139
|
+
| `onTokenExpired` | Token expired and refresh failed |
|
|
140
|
+
|
|
141
|
+
## Silent SSO
|
|
142
|
+
|
|
143
|
+
For silent SSO to work, serve this HTML file at `/silent-check-sso.html`:
|
|
144
|
+
|
|
145
|
+
```html
|
|
146
|
+
<!doctype html>
|
|
147
|
+
<html>
|
|
148
|
+
<body>
|
|
149
|
+
<script>
|
|
150
|
+
parent.postMessage(location.href, location.origin);
|
|
151
|
+
</script>
|
|
152
|
+
</body>
|
|
153
|
+
</html>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { FjordAuthOptions, FjordUser, FjordAuthEvent, FjordAuthEventCallback } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* FjordAuth — a lightweight, developer-friendly wrapper around Keycloak OIDC.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { FjordAuth } from '@fjordid/client';
|
|
8
|
+
*
|
|
9
|
+
* const auth = new FjordAuth({
|
|
10
|
+
* domain: 'your-app.fjordid.eu',
|
|
11
|
+
* clientId: 'your-client-id',
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* await auth.init();
|
|
15
|
+
* auth.login();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare class FjordAuth {
|
|
19
|
+
private readonly kc;
|
|
20
|
+
private readonly options;
|
|
21
|
+
private initialized;
|
|
22
|
+
private initPromise;
|
|
23
|
+
private listeners;
|
|
24
|
+
/** The currently authenticated user, or `null` if not logged in. */
|
|
25
|
+
user: FjordUser | null;
|
|
26
|
+
constructor(options: FjordAuthOptions);
|
|
27
|
+
/**
|
|
28
|
+
* Initialize authentication. Performs a silent SSO check to see if the user
|
|
29
|
+
* is already logged in.
|
|
30
|
+
*
|
|
31
|
+
* @returns `true` if the user is authenticated, `false` otherwise.
|
|
32
|
+
*/
|
|
33
|
+
init(): Promise<boolean>;
|
|
34
|
+
private doInit;
|
|
35
|
+
/** Whether the user is currently authenticated. */
|
|
36
|
+
get authenticated(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Redirect to the FjordID login page.
|
|
39
|
+
*
|
|
40
|
+
* @param redirectUri — Override the post-login redirect. Defaults to `options.redirectUri`.
|
|
41
|
+
*/
|
|
42
|
+
login(redirectUri?: string): void;
|
|
43
|
+
/**
|
|
44
|
+
* Redirect to the FjordID registration page.
|
|
45
|
+
*
|
|
46
|
+
* @param redirectUri — Override the post-registration redirect.
|
|
47
|
+
*/
|
|
48
|
+
register(redirectUri?: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Log out and redirect to the post-logout URL.
|
|
51
|
+
*
|
|
52
|
+
* @param redirectUri — Override the post-logout redirect.
|
|
53
|
+
*/
|
|
54
|
+
logout(redirectUri?: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Get a valid access token, refreshing if needed.
|
|
57
|
+
*
|
|
58
|
+
* @returns The access token string, or `undefined` if not authenticated.
|
|
59
|
+
*/
|
|
60
|
+
getToken(): Promise<string | undefined>;
|
|
61
|
+
/**
|
|
62
|
+
* Force-refresh the access token.
|
|
63
|
+
*
|
|
64
|
+
* @returns The new access token string, or `undefined` on failure.
|
|
65
|
+
*/
|
|
66
|
+
refreshToken(): Promise<string | undefined>;
|
|
67
|
+
/** The raw access token, or `undefined`. */
|
|
68
|
+
get token(): string | undefined;
|
|
69
|
+
/** The raw ID token, or `undefined`. */
|
|
70
|
+
get idToken(): string | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe to an auth event.
|
|
73
|
+
*
|
|
74
|
+
* @returns An unsubscribe function.
|
|
75
|
+
*/
|
|
76
|
+
on(event: FjordAuthEvent, callback: FjordAuthEventCallback): () => void;
|
|
77
|
+
private emit;
|
|
78
|
+
private syncUser;
|
|
79
|
+
/**
|
|
80
|
+
* Validate that a redirect URI is a real URL and not an "undefined" string
|
|
81
|
+
* produced by an unset environment variable.
|
|
82
|
+
*/
|
|
83
|
+
private assertValidRedirectUri;
|
|
84
|
+
/**
|
|
85
|
+
* Keycloak may lose authServerUrl after an init timeout.
|
|
86
|
+
* Ensure it's always set correctly before building redirect URLs.
|
|
87
|
+
*/
|
|
88
|
+
private ensureAuthServerUrl;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=FjordAuth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FjordAuth.d.ts","sourceRoot":"","sources":["../src/FjordAuth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAChB,SAAS,EACT,cAAc,EACd,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAOpB;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAW;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAYL;IAEnB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,SAAS,CAA0D;IAE3E,oEAAoE;IACpE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAQ;gBAElB,OAAO,EAAE,gBAAgB;IA6DrC;;;;;OAKG;IACG,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;YAahB,MAAM;IA0DpB,mDAAmD;IACnD,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED;;;;OAIG;IACH,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAOjC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAOpC;;;;OAIG;IACH,MAAM,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAWlC;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAU7C;;;;OAIG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAWjD,4CAA4C;IAC5C,IAAI,KAAK,IAAI,MAAM,GAAG,SAAS,CAE9B;IAED,wCAAwC;IACxC,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAMD;;;;OAIG;IACH,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,sBAAsB,GAAG,MAAM,IAAI;IAQvE,OAAO,CAAC,IAAI;IAcZ,OAAO,CAAC,QAAQ;IA2BhB;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAe9B;;;OAGG;IACH,OAAO,CAAC,mBAAmB;CAS5B"}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import Keycloak from "keycloak-js";
|
|
2
|
+
const DEFAULT_AUTH_URL = "https://auth.fjordid.eu";
|
|
3
|
+
const DEFAULT_REALM = "fjordid";
|
|
4
|
+
const DEFAULT_INIT_TIMEOUT = 10_000;
|
|
5
|
+
const DEFAULT_TOKEN_REFRESH_BUFFER = 30;
|
|
6
|
+
/**
|
|
7
|
+
* FjordAuth — a lightweight, developer-friendly wrapper around Keycloak OIDC.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { FjordAuth } from '@fjordid/client';
|
|
12
|
+
*
|
|
13
|
+
* const auth = new FjordAuth({
|
|
14
|
+
* domain: 'your-app.fjordid.eu',
|
|
15
|
+
* clientId: 'your-client-id',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* await auth.init();
|
|
19
|
+
* auth.login();
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export class FjordAuth {
|
|
23
|
+
kc;
|
|
24
|
+
options;
|
|
25
|
+
initialized = false;
|
|
26
|
+
initPromise = null;
|
|
27
|
+
listeners = new Map();
|
|
28
|
+
/** The currently authenticated user, or `null` if not logged in. */
|
|
29
|
+
user = null;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
// Validate required config up-front so developers get a clear error
|
|
32
|
+
// instead of Keycloak's generic "Page not found".
|
|
33
|
+
if (!options.clientId || options.clientId.trim() === "") {
|
|
34
|
+
throw new Error("[FjordAuth] Missing clientId. " +
|
|
35
|
+
"Make sure you pass a valid clientId when creating FjordAuth. " +
|
|
36
|
+
"If you are reading it from an environment variable, " +
|
|
37
|
+
"ensure the variable is set before the app starts.");
|
|
38
|
+
}
|
|
39
|
+
this.options = {
|
|
40
|
+
...options,
|
|
41
|
+
authUrl: options.authUrl ?? DEFAULT_AUTH_URL,
|
|
42
|
+
realm: options.realm ?? DEFAULT_REALM,
|
|
43
|
+
redirectUri: options.redirectUri ??
|
|
44
|
+
(typeof window !== "undefined" ? window.location.origin : ""),
|
|
45
|
+
postLogoutRedirectUri: options.postLogoutRedirectUri ??
|
|
46
|
+
(typeof window !== "undefined" ? window.location.origin : ""),
|
|
47
|
+
silentSso: options.silentSso ?? true,
|
|
48
|
+
initTimeout: options.initTimeout ?? DEFAULT_INIT_TIMEOUT,
|
|
49
|
+
tokenRefreshBuffer: options.tokenRefreshBuffer ?? DEFAULT_TOKEN_REFRESH_BUFFER,
|
|
50
|
+
};
|
|
51
|
+
// Validate redirectUri after defaults are applied
|
|
52
|
+
this.assertValidRedirectUri(this.options.redirectUri, "redirectUri");
|
|
53
|
+
this.assertValidRedirectUri(this.options.postLogoutRedirectUri, "postLogoutRedirectUri");
|
|
54
|
+
this.kc = new Keycloak({
|
|
55
|
+
url: this.options.authUrl,
|
|
56
|
+
realm: this.options.realm,
|
|
57
|
+
clientId: this.options.clientId,
|
|
58
|
+
});
|
|
59
|
+
// Wire up Keycloak events
|
|
60
|
+
this.kc.onReady = () => this.emit("onReady");
|
|
61
|
+
this.kc.onAuthSuccess = () => {
|
|
62
|
+
this.syncUser();
|
|
63
|
+
this.emit("onAuthSuccess");
|
|
64
|
+
};
|
|
65
|
+
this.kc.onAuthLogout = () => {
|
|
66
|
+
this.user = null;
|
|
67
|
+
this.emit("onAuthLogout");
|
|
68
|
+
};
|
|
69
|
+
this.kc.onAuthError = () => this.emit("onAuthError");
|
|
70
|
+
this.kc.onTokenExpired = () => {
|
|
71
|
+
this.refreshToken().catch(() => this.emit("onTokenExpired"));
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Lifecycle
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
/**
|
|
78
|
+
* Initialize authentication. Performs a silent SSO check to see if the user
|
|
79
|
+
* is already logged in.
|
|
80
|
+
*
|
|
81
|
+
* @returns `true` if the user is authenticated, `false` otherwise.
|
|
82
|
+
*/
|
|
83
|
+
async init() {
|
|
84
|
+
if (this.initialized) {
|
|
85
|
+
return this.authenticated;
|
|
86
|
+
}
|
|
87
|
+
if (this.initPromise) {
|
|
88
|
+
return this.initPromise;
|
|
89
|
+
}
|
|
90
|
+
this.initPromise = this.doInit();
|
|
91
|
+
return this.initPromise;
|
|
92
|
+
}
|
|
93
|
+
async doInit() {
|
|
94
|
+
try {
|
|
95
|
+
const authenticated = await withTimeout(this.kc.init({
|
|
96
|
+
onLoad: "check-sso",
|
|
97
|
+
pkceMethod: "S256",
|
|
98
|
+
checkLoginIframe: false,
|
|
99
|
+
silentCheckSsoRedirectUri: this.options.silentSso
|
|
100
|
+
? window.location.origin + "/silent-check-sso.html"
|
|
101
|
+
: undefined,
|
|
102
|
+
}), this.options.initTimeout, "FjordAuth init timeout — SSO check took too long");
|
|
103
|
+
this.initialized = true;
|
|
104
|
+
if (authenticated) {
|
|
105
|
+
this.syncUser();
|
|
106
|
+
}
|
|
107
|
+
return authenticated;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
111
|
+
// Keycloak double-init guard
|
|
112
|
+
if (msg.includes("can only be initialized once")) {
|
|
113
|
+
this.initialized = true;
|
|
114
|
+
this.syncUser();
|
|
115
|
+
return this.authenticated;
|
|
116
|
+
}
|
|
117
|
+
// Timeout / 3rd-party cookie issues — degrade gracefully
|
|
118
|
+
const isRecoverable = msg.toLowerCase().includes("timeout") ||
|
|
119
|
+
msg.includes("3rd party") ||
|
|
120
|
+
msg.includes("third party") ||
|
|
121
|
+
msg.includes("iframe");
|
|
122
|
+
if (isRecoverable) {
|
|
123
|
+
console.warn("[FjordAuth] SSO check failed (3rd-party cookies likely blocked), proceeding without SSO:", msg);
|
|
124
|
+
this.initialized = true;
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
console.error("[FjordAuth] Initialization failed:", error);
|
|
128
|
+
this.initialized = true;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Auth actions
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
/** Whether the user is currently authenticated. */
|
|
136
|
+
get authenticated() {
|
|
137
|
+
return this.kc.authenticated ?? false;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Redirect to the FjordID login page.
|
|
141
|
+
*
|
|
142
|
+
* @param redirectUri — Override the post-login redirect. Defaults to `options.redirectUri`.
|
|
143
|
+
*/
|
|
144
|
+
login(redirectUri) {
|
|
145
|
+
const uri = redirectUri ?? this.options.redirectUri;
|
|
146
|
+
this.assertValidRedirectUri(uri, "redirectUri");
|
|
147
|
+
this.ensureAuthServerUrl();
|
|
148
|
+
this.kc.login({ redirectUri: uri });
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Redirect to the FjordID registration page.
|
|
152
|
+
*
|
|
153
|
+
* @param redirectUri — Override the post-registration redirect.
|
|
154
|
+
*/
|
|
155
|
+
register(redirectUri) {
|
|
156
|
+
const uri = redirectUri ?? this.options.redirectUri;
|
|
157
|
+
this.assertValidRedirectUri(uri, "redirectUri");
|
|
158
|
+
this.ensureAuthServerUrl();
|
|
159
|
+
this.kc.register({ redirectUri: uri });
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Log out and redirect to the post-logout URL.
|
|
163
|
+
*
|
|
164
|
+
* @param redirectUri — Override the post-logout redirect.
|
|
165
|
+
*/
|
|
166
|
+
logout(redirectUri) {
|
|
167
|
+
this.user = null;
|
|
168
|
+
this.kc.logout({
|
|
169
|
+
redirectUri: redirectUri ?? this.options.postLogoutRedirectUri,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Token management
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
/**
|
|
176
|
+
* Get a valid access token, refreshing if needed.
|
|
177
|
+
*
|
|
178
|
+
* @returns The access token string, or `undefined` if not authenticated.
|
|
179
|
+
*/
|
|
180
|
+
async getToken() {
|
|
181
|
+
try {
|
|
182
|
+
await this.kc.updateToken(this.options.tokenRefreshBuffer);
|
|
183
|
+
return this.kc.token;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
console.error("[FjordAuth] Failed to refresh token");
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Force-refresh the access token.
|
|
192
|
+
*
|
|
193
|
+
* @returns The new access token string, or `undefined` on failure.
|
|
194
|
+
*/
|
|
195
|
+
async refreshToken() {
|
|
196
|
+
try {
|
|
197
|
+
await this.kc.updateToken(-1); // Force refresh
|
|
198
|
+
this.syncUser();
|
|
199
|
+
return this.kc.token;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
console.error("[FjordAuth] Token refresh failed");
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** The raw access token, or `undefined`. */
|
|
207
|
+
get token() {
|
|
208
|
+
return this.kc.token;
|
|
209
|
+
}
|
|
210
|
+
/** The raw ID token, or `undefined`. */
|
|
211
|
+
get idToken() {
|
|
212
|
+
return this.kc.idToken;
|
|
213
|
+
}
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Events
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
/**
|
|
218
|
+
* Subscribe to an auth event.
|
|
219
|
+
*
|
|
220
|
+
* @returns An unsubscribe function.
|
|
221
|
+
*/
|
|
222
|
+
on(event, callback) {
|
|
223
|
+
if (!this.listeners.has(event)) {
|
|
224
|
+
this.listeners.set(event, new Set());
|
|
225
|
+
}
|
|
226
|
+
this.listeners.get(event).add(callback);
|
|
227
|
+
return () => this.listeners.get(event)?.delete(callback);
|
|
228
|
+
}
|
|
229
|
+
emit(event) {
|
|
230
|
+
this.listeners.get(event)?.forEach((cb) => {
|
|
231
|
+
try {
|
|
232
|
+
cb();
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
console.error(`[FjordAuth] Error in ${event} handler:`, e);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Internals
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
syncUser() {
|
|
243
|
+
const profile = this.kc.tokenParsed;
|
|
244
|
+
if (profile) {
|
|
245
|
+
this.user = {
|
|
246
|
+
id: profile.sub ?? "",
|
|
247
|
+
email: profile.email ?? "",
|
|
248
|
+
name: profile.name ??
|
|
249
|
+
[
|
|
250
|
+
profile.given_name,
|
|
251
|
+
profile.family_name,
|
|
252
|
+
]
|
|
253
|
+
.filter(Boolean)
|
|
254
|
+
.join(" "),
|
|
255
|
+
firstName: profile.given_name,
|
|
256
|
+
lastName: profile.family_name,
|
|
257
|
+
emailVerified: profile.email_verified,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Validate that a redirect URI is a real URL and not an "undefined" string
|
|
263
|
+
* produced by an unset environment variable.
|
|
264
|
+
*/
|
|
265
|
+
assertValidRedirectUri(uri, name) {
|
|
266
|
+
if (!uri ||
|
|
267
|
+
uri === "undefined" ||
|
|
268
|
+
uri.startsWith("undefined/") ||
|
|
269
|
+
uri.startsWith("undefined%2F")) {
|
|
270
|
+
throw new Error(`[FjordAuth] Invalid ${name}: "${uri}". ` +
|
|
271
|
+
"This usually means an environment variable used to build the redirect URL is not set. " +
|
|
272
|
+
"Check that all VITE_* / process.env.* variables are defined before the app starts.");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Keycloak may lose authServerUrl after an init timeout.
|
|
277
|
+
* Ensure it's always set correctly before building redirect URLs.
|
|
278
|
+
*/
|
|
279
|
+
ensureAuthServerUrl() {
|
|
280
|
+
if (!this.kc.authServerUrl ||
|
|
281
|
+
this.kc.authServerUrl !== this.options.authUrl) {
|
|
282
|
+
this.kc.authServerUrl =
|
|
283
|
+
this.options.authUrl;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Helpers
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
function withTimeout(promise, ms, message) {
|
|
291
|
+
return Promise.race([
|
|
292
|
+
promise,
|
|
293
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms)),
|
|
294
|
+
]);
|
|
295
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,cAAc,EACd,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FjordAuth } from "./FjordAuth.js";
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration types for the FjordID client.
|
|
3
|
+
*/
|
|
4
|
+
/** Options for initializing a FjordAuth instance. */
|
|
5
|
+
export interface FjordAuthOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Your FjordID domain, e.g. `"your-app.fjordid.eu"`.
|
|
8
|
+
* Used to derive the Keycloak auth URL and realm.
|
|
9
|
+
*/
|
|
10
|
+
domain: string;
|
|
11
|
+
/** The OIDC client ID for your application. */
|
|
12
|
+
clientId: string;
|
|
13
|
+
/**
|
|
14
|
+
* Override the Keycloak auth server URL.
|
|
15
|
+
* Defaults to `https://auth.fjordid.eu`.
|
|
16
|
+
*/
|
|
17
|
+
authUrl?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The Keycloak realm name.
|
|
20
|
+
* Defaults to `"fjordid"`.
|
|
21
|
+
*/
|
|
22
|
+
realm?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Where to redirect after login. Defaults to `window.location.origin`.
|
|
25
|
+
*/
|
|
26
|
+
redirectUri?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Where to redirect after logout. Defaults to `window.location.origin`.
|
|
29
|
+
*/
|
|
30
|
+
postLogoutRedirectUri?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Enable silent SSO check via hidden iframe.
|
|
33
|
+
* Defaults to `true`. Set to `false` if 3rd-party cookies are blocked.
|
|
34
|
+
*/
|
|
35
|
+
silentSso?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Timeout in milliseconds for the initial SSO check.
|
|
38
|
+
* Defaults to `10000` (10 seconds).
|
|
39
|
+
*/
|
|
40
|
+
initTimeout?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Number of seconds before token expiry to trigger a refresh.
|
|
43
|
+
* Defaults to `30`.
|
|
44
|
+
*/
|
|
45
|
+
tokenRefreshBuffer?: number;
|
|
46
|
+
}
|
|
47
|
+
/** Authenticated user profile. */
|
|
48
|
+
export interface FjordUser {
|
|
49
|
+
/** Keycloak subject ID (UUID). */
|
|
50
|
+
id: string;
|
|
51
|
+
/** User's email address. */
|
|
52
|
+
email: string;
|
|
53
|
+
/** User's full name (may be empty). */
|
|
54
|
+
name: string;
|
|
55
|
+
/** User's first name. */
|
|
56
|
+
firstName?: string;
|
|
57
|
+
/** User's last name. */
|
|
58
|
+
lastName?: string;
|
|
59
|
+
/** Whether the email has been verified. */
|
|
60
|
+
emailVerified?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** Events emitted by FjordAuth. */
|
|
63
|
+
export type FjordAuthEvent = "onReady" | "onAuthSuccess" | "onAuthLogout" | "onAuthError" | "onTokenExpired";
|
|
64
|
+
/** Callback signature for auth events. */
|
|
65
|
+
export type FjordAuthEventCallback = () => void;
|
|
66
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,qDAAqD;AACrD,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,kCAAkC;AAClC,MAAM,WAAW,SAAS;IACxB,kCAAkC;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wBAAwB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,mCAAmC;AACnC,MAAM,MAAM,cAAc,GACtB,SAAS,GACT,eAAe,GACf,cAAc,GACd,aAAa,GACb,gBAAgB,CAAC;AAErB,0CAA0C;AAC1C,MAAM,MAAM,sBAAsB,GAAG,MAAM,IAAI,CAAC"}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fjordid/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight JavaScript/TypeScript client for FjordID authentication. Wraps Keycloak OIDC with a simple, developer-friendly API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"dev": "tsc --watch"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/terjetyl/FjordId.git",
|
|
26
|
+
"directory": "packages/client"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://fjordid.eu/docs",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/terjetyl/FjordId/issues"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org/"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"auth",
|
|
38
|
+
"authentication",
|
|
39
|
+
"oidc",
|
|
40
|
+
"oauth2",
|
|
41
|
+
"keycloak",
|
|
42
|
+
"fjordid",
|
|
43
|
+
"gdpr",
|
|
44
|
+
"eu",
|
|
45
|
+
"identity"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"keycloak-js": "^26.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"typescript": "^5.7.3"
|
|
53
|
+
}
|
|
54
|
+
}
|