@dehwyyy/auth 1.0.7 → 2.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 +185 -0
- package/dist/v2/auth/client/baseClient.d.ts +2 -2
- package/dist/v2/auth/client/baseClient.js +6 -3
- package/dist/v2/auth/client/client.d.ts +2 -2
- package/dist/v2/auth/client/client.js +5 -5
- package/dist/v2/auth/client/index.d.ts +2 -2
- package/dist/v2/auth/client/index.js +2 -2
- package/dist/v2/auth/factory.d.ts +20 -0
- package/dist/v2/auth/factory.js +31 -0
- package/dist/v2/auth/service/baseService.d.ts +3 -6
- package/dist/v2/auth/service/baseService.js +45 -17
- package/dist/v2/auth/service/index.d.ts +2 -2
- package/dist/v2/auth/service/index.js +2 -2
- package/dist/v2/auth/service/service.d.ts +15 -6
- package/dist/v2/auth/service/service.js +142 -28
- package/dist/v2/client/builder/index.d.ts +1 -1
- package/dist/v2/client/builder/index.js +1 -1
- package/dist/v2/client/client.d.ts +1 -7
- package/dist/v2/client/client.js +0 -14
- package/dist/v2/client/index.d.ts +3 -3
- package/dist/v2/client/index.js +3 -3
- package/dist/v2/client/middleware/builder.d.ts +6 -2
- package/dist/v2/client/middleware/builder.js +26 -8
- package/dist/v2/client/middleware/index.d.ts +1 -1
- package/dist/v2/client/middleware/index.js +1 -1
- package/dist/v2/client/middleware/middleware.d.ts +2 -2
- package/dist/v2/client/middleware/middleware.js +3 -3
- package/dist/v2/index.d.ts +11 -5
- package/dist/v2/index.js +7 -5
- package/dist/v2/pkg/errors/errors.d.ts +5 -0
- package/dist/v2/pkg/errors/errors.js +9 -0
- package/dist/v2/pkg/errors/index.d.ts +4 -0
- package/dist/v2/pkg/errors/index.js +2 -0
- package/dist/v2/pkg/localStorage/index.d.ts +2 -2
- package/dist/v2/pkg/localStorage/index.js +2 -2
- package/dist/v2/pkg/localStorage/localStorage.d.ts +13 -1
- package/dist/v2/pkg/localStorage/localStorage.js +19 -0
- package/dist/v2/pkg/pkce/index.d.ts +2 -0
- package/dist/v2/pkg/pkce/index.js +2 -0
- package/dist/v2/pkg/pkce/pkce.d.ts +3 -0
- package/dist/v2/pkg/pkce/pkce.js +30 -0
- package/dist/v2/pkg/types/authService.d.ts +34 -3
- package/dist/v2/pkg/types/index.d.ts +3 -3
- package/dist/v2/pkg/url/index.d.ts +2 -0
- package/dist/v2/pkg/url/index.js +2 -0
- package/dist/v2/pkg/url/url.d.ts +8 -0
- package/dist/v2/pkg/url/url.js +40 -0
- package/dist/v2/vue/router-guard/guard.d.ts +10 -7
- package/dist/v2/vue/router-guard/guard.js +46 -13
- package/dist/v2/vue/router-guard/index.d.ts +1 -1
- package/dist/v2/vue/router-guard/index.js +1 -1
- package/package.json +14 -12
- package/dist/client/client.d.ts +0 -2
- package/dist/client/client.js +0 -14
- package/dist/client/middleware.d.ts +0 -15
- package/dist/client/middleware.js +0 -51
- package/dist/client/storage/localStorage.d.ts +0 -8
- package/dist/client/storage/localStorage.js +0 -15
- package/dist/guard/index.d.ts +0 -11
- package/dist/guard/index.js +0 -62
- package/dist/index.d.ts +0 -26
- package/dist/index.js +0 -160
- package/dist/types.d.ts +0 -16
- package/dist/types.js +0 -2
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# @dehwyyy/auth
|
|
2
|
+
|
|
3
|
+
Browser-side OIDC auth client for the Paylonium SPAs. Talks directly to Zitadel
|
|
4
|
+
using Authorization Code + PKCE (S256). No backend auth proxy.
|
|
5
|
+
|
|
6
|
+
- ESM only, single runtime dependency: `openapi-fetch`.
|
|
7
|
+
- PKCE via WebCrypto (no extra deps).
|
|
8
|
+
- Tokens in `localStorage` (`accessToken` / `refreshToken` / `idToken`).
|
|
9
|
+
- PKCE `verifier` / `state` / `returnTo` in `sessionStorage`.
|
|
10
|
+
- Refresh-token rotation with single-flight and cross-tab compare-and-clear.
|
|
11
|
+
- Vue Router guard with role checks and an OIDC-callback anti-loop.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @dehwyyy/auth
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Both `@dehwyyy/auth` and `@dehwyyy/auth/v2` resolve to the same v2 entrypoint.
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import type { ZitadelConfig } from "@dehwyyy/auth"
|
|
25
|
+
|
|
26
|
+
const config: ZitadelConfig = {
|
|
27
|
+
issuer: "https://core.auth.my-paylonium.com",
|
|
28
|
+
clientId: "376852297126373720",
|
|
29
|
+
redirectUri: window.location.origin + "/auth/callback",
|
|
30
|
+
postLogoutRedirectUri: window.location.origin + "/",
|
|
31
|
+
// scopes is optional; this is the default:
|
|
32
|
+
// ["openid","profile","email","offline_access",
|
|
33
|
+
// "urn:zitadel:iam:org:projects:roles","urn:zitadel:iam:user:metadata"]
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Wiring with `createAuth`
|
|
38
|
+
|
|
39
|
+
`createAuth` is the single wiring entry point. It builds the refresh client, the
|
|
40
|
+
authenticated API client (access-token attach + auto-refresh on 401), the
|
|
41
|
+
services, and a `MiddlewareBuilder` you can reuse for your own API clients.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { createAuth } from "@dehwyyy/auth"
|
|
45
|
+
|
|
46
|
+
export const auth = createAuth(config)
|
|
47
|
+
// auth.authService — Login / HandleCallback / GetMe / Logout
|
|
48
|
+
// auth.baseAuthService — Refresh / RequestWithAccessToken
|
|
49
|
+
// auth.middlewareBuilder — for wiring your own openapi-fetch clients
|
|
50
|
+
// auth.callbackPath — derived from config.redirectUri (e.g. "/auth/callback")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Reuse the middleware for your domain API client:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import createClient from "openapi-fetch"
|
|
57
|
+
import type { paths } from "./api/schema"
|
|
58
|
+
|
|
59
|
+
export const api = createClient<paths>({ baseUrl: import.meta.env.VITE_API_URL })
|
|
60
|
+
api.use(...auth.middlewareBuilder.buildDefault())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Login
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// returnTo is optional; pass the path you want to land on after auth.
|
|
67
|
+
await auth.authService.Login(router.currentRoute.value.fullPath)
|
|
68
|
+
// redirects the browser to Zitadel's /oauth/v2/authorize
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Callback page
|
|
72
|
+
|
|
73
|
+
Mount this on `config.redirectUri` (e.g. `/auth/callback`). It performs the
|
|
74
|
+
token exchange, fetches the user, and redirects to `returnTo`.
|
|
75
|
+
|
|
76
|
+
```vue
|
|
77
|
+
<script setup lang="ts">
|
|
78
|
+
import { onMounted } from "vue"
|
|
79
|
+
import { useRouter } from "vue-router"
|
|
80
|
+
import { CallbackError } from "@dehwyyy/auth"
|
|
81
|
+
import { auth } from "@/auth"
|
|
82
|
+
|
|
83
|
+
const router = useRouter()
|
|
84
|
+
|
|
85
|
+
onMounted(async () => {
|
|
86
|
+
try {
|
|
87
|
+
const { returnTo } = await auth.authService.HandleCallback()
|
|
88
|
+
// returnTo is already same-origin/relative-validated by the library
|
|
89
|
+
// (a foreign origin is dropped to null), so it is safe to navigate to.
|
|
90
|
+
await router.replace(returnTo ?? "/")
|
|
91
|
+
} catch (e) {
|
|
92
|
+
if (e instanceof CallbackError) {
|
|
93
|
+
// e.code: "state_mismatch" | "verifier_missing" | "code_missing" | "exchange_failed"
|
|
94
|
+
console.error("auth callback failed:", e.code)
|
|
95
|
+
}
|
|
96
|
+
await router.replace("/")
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<template>
|
|
102
|
+
<div>Signing you in…</div>
|
|
103
|
+
</template>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`HandleCallback` is idempotent per authorization code: a double mount (Vue dev
|
|
107
|
+
StrictMode) or an F5 on the callback URL reuses the in-flight/resolved exchange
|
|
108
|
+
instead of failing with a state mismatch.
|
|
109
|
+
|
|
110
|
+
## Route guard
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { RouterAuthGuard } from "@dehwyyy/auth"
|
|
114
|
+
import { auth } from "@/auth"
|
|
115
|
+
|
|
116
|
+
const guard = new RouterAuthGuard(auth.authService, {
|
|
117
|
+
callbackPath: auth.callbackPath, // required — only this route may carry ?code&state
|
|
118
|
+
fallbackPage: "/forbidden",
|
|
119
|
+
baseServiceURL: window.location.origin,
|
|
120
|
+
onGetMe: (user) => { /* hydrate your store */ },
|
|
121
|
+
// cacheTTL is optional, defaults to 15000 (ms)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
router.beforeEach(guard.Authorized(["ADMIN", "DEV"])) // require one of these roles
|
|
125
|
+
// or guard.Authorized() to require only authentication
|
|
126
|
+
// or guard.Unauthorized([{ to: "/dashboard", roles: ["MERCHANT"] }]) on public pages
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Security note: the guard only treats a navigation as the OIDC callback when the
|
|
130
|
+
target path equals `callbackPath` **and** `code`+`state` are present. Appending
|
|
131
|
+
`?code&state` to any other protected route does not bypass the guard.
|
|
132
|
+
|
|
133
|
+
## Logout
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
await auth.authService.Logout()
|
|
137
|
+
// clears tokens and redirects to Zitadel /oidc/v1/end_session
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Composability (without `createAuth`)
|
|
141
|
+
|
|
142
|
+
The pieces remain individually exported for custom wiring:
|
|
143
|
+
`AuthService`, `BaseAuthService`, `ClientBuilder`, `ClientInstance`,
|
|
144
|
+
`MiddlewareBuilder`, `CreateAuthClientInstance`, `BaseAuthClientInstance`,
|
|
145
|
+
`RouterAuthGuard`, `CallbackError`, plus types
|
|
146
|
+
`ZitadelConfig`, `GetMeResponse`, `VerboseUserInfo`, `HandleCallbackResult`,
|
|
147
|
+
`RouteLocation`, `CallbackErrorCode`, `Auth`.
|
|
148
|
+
|
|
149
|
+
### Footgun: `BaseAuthClientInstance` is a process-wide singleton
|
|
150
|
+
|
|
151
|
+
`BaseAuthClientInstance(issuer)` caches its client on first call — the first
|
|
152
|
+
`issuer` wins for the module's lifetime, and later calls with a different issuer
|
|
153
|
+
return the original client. For a single-issuer SPA this is fine. If you need
|
|
154
|
+
multiple issuers (or isolated instances in tests), use `createAuth` instead,
|
|
155
|
+
which constructs fresh clients each call.
|
|
156
|
+
|
|
157
|
+
## GetMe response shape
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
type GetMeResponse = {
|
|
161
|
+
user_id: string
|
|
162
|
+
roles: string[] // keys of the Zitadel project-roles claim
|
|
163
|
+
active: boolean // always true when a userinfo response is returned
|
|
164
|
+
info?: { // populated only when verbose
|
|
165
|
+
username?: string
|
|
166
|
+
email?: string
|
|
167
|
+
email_verified?: boolean
|
|
168
|
+
name?: string
|
|
169
|
+
picture?: string
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
bun install
|
|
178
|
+
bun run gen # regenerate src/v2/pkg/types/schema.d.ts from spec/zitadel-oidc.yaml
|
|
179
|
+
bun run build # tsc + tsc-alias (full-path ESM resolution)
|
|
180
|
+
bun test
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The OIDC surface (`/oauth/v2/token`, `/oidc/v1/userinfo`, `/oidc/v1/end_session`)
|
|
184
|
+
is described in `spec/zitadel-oidc.yaml`; all schema types are generated from it
|
|
185
|
+
via `openapi-typescript`. Do not hand-edit `schema.d.ts`.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { HttpAuthClient } from '../../../v2/pkg/types';
|
|
2
|
-
export declare const BaseAuthClientInstance: (
|
|
1
|
+
import { HttpAuthClient } from '../../../v2/pkg/types/index.js';
|
|
2
|
+
export declare const BaseAuthClientInstance: (issuer: string) => HttpAuthClient;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { normalizeIssuer } from '../../../v2/pkg/url/index.js';
|
|
1
2
|
import createClient from 'openapi-fetch';
|
|
2
3
|
let baseClient = null;
|
|
3
|
-
|
|
4
|
+
// NOTE: process-wide singleton — the first issuer wins for the lifetime of the
|
|
5
|
+
// module. Re-calling with a different issuer returns the original client. Fine
|
|
6
|
+
// for the single-issuer SPA setups this package targets; see README footgun.
|
|
7
|
+
export const BaseAuthClientInstance = (issuer) => {
|
|
4
8
|
if (!baseClient) {
|
|
5
9
|
baseClient = createClient({
|
|
6
|
-
baseUrl,
|
|
7
|
-
credentials: 'include',
|
|
10
|
+
baseUrl: normalizeIssuer(issuer),
|
|
8
11
|
});
|
|
9
12
|
}
|
|
10
13
|
return baseClient;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { BaseAuthService } from "../../../v2/pkg/types";
|
|
1
|
+
import { BaseAuthService } from "../../../v2/pkg/types/index.js";
|
|
2
2
|
import { paths } from "@dehwyyy/auth/v2/pkg/types/schema";
|
|
3
|
-
export declare const CreateAuthClientInstance: (
|
|
3
|
+
export declare const CreateAuthClientInstance: (issuer: string, baseAuthService: BaseAuthService, fetchImpl?: typeof fetch) => import("openapi-fetch").Client<paths, `${string}/${string}`>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ClientBuilder, MiddlewareBuilder } from "../../../v2/client";
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { ClientBuilder, MiddlewareBuilder } from "../../../v2/client/index.js";
|
|
2
|
+
import { normalizeIssuer } from "../../../v2/pkg/url/index.js";
|
|
3
|
+
export const CreateAuthClientInstance = (issuer, baseAuthService, fetchImpl = fetch) => new ClientBuilder().
|
|
4
|
+
withMiddleware(...new MiddlewareBuilder(baseAuthService, fetchImpl).buildDefault()).
|
|
4
5
|
build({
|
|
5
|
-
baseUrl:
|
|
6
|
-
credentials: 'include',
|
|
6
|
+
baseUrl: normalizeIssuer(issuer),
|
|
7
7
|
});
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { BaseAuthClientInstance } from "../../../v2/auth/client/baseClient";
|
|
2
|
-
import { CreateAuthClientInstance } from "../../../v2/auth/client/client";
|
|
1
|
+
import { BaseAuthClientInstance } from "../../../v2/auth/client/baseClient.js";
|
|
2
|
+
import { CreateAuthClientInstance } from "../../../v2/auth/client/client.js";
|
|
3
3
|
export { BaseAuthClientInstance, CreateAuthClientInstance };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { BaseAuthClientInstance } from "../../../v2/auth/client/baseClient";
|
|
2
|
-
import { CreateAuthClientInstance } from "../../../v2/auth/client/client";
|
|
1
|
+
import { BaseAuthClientInstance } from "../../../v2/auth/client/baseClient.js";
|
|
2
|
+
import { CreateAuthClientInstance } from "../../../v2/auth/client/client.js";
|
|
3
3
|
export { BaseAuthClientInstance, CreateAuthClientInstance };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseAuthService } from "../../v2/auth/service/baseService.js";
|
|
2
|
+
import { AuthService } from "../../v2/auth/service/service.js";
|
|
3
|
+
import { MiddlewareBuilder } from "../../v2/client/index.js";
|
|
4
|
+
import { ZitadelConfig } from "../../v2/pkg/types/index.js";
|
|
5
|
+
export type Auth = {
|
|
6
|
+
authService: AuthService;
|
|
7
|
+
baseAuthService: BaseAuthService;
|
|
8
|
+
middlewareBuilder: MiddlewareBuilder;
|
|
9
|
+
callbackPath: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Single wiring entry point for the three SPA frontends. Builds:
|
|
13
|
+
* - a bare token client for refresh (no auto-refresh middleware to avoid
|
|
14
|
+
* a refresh -> 401 -> refresh loop),
|
|
15
|
+
* - the BaseAuthService driving that client,
|
|
16
|
+
* - an authenticated client (access token attach + auto-refresh on 401),
|
|
17
|
+
* - the AuthService over it,
|
|
18
|
+
* - a MiddlewareBuilder for callers wiring their own API clients.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createAuth(config: ZitadelConfig, fetchImpl?: typeof fetch): Auth;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BaseAuthService } from "../../v2/auth/service/baseService.js";
|
|
2
|
+
import { AuthService } from "../../v2/auth/service/service.js";
|
|
3
|
+
import { ClientBuilder, MiddlewareBuilder } from "../../v2/client/index.js";
|
|
4
|
+
import { normalizeIssuer, pathnameOf } from "../../v2/pkg/url/index.js";
|
|
5
|
+
import createClient from "openapi-fetch";
|
|
6
|
+
/**
|
|
7
|
+
* Single wiring entry point for the three SPA frontends. Builds:
|
|
8
|
+
* - a bare token client for refresh (no auto-refresh middleware to avoid
|
|
9
|
+
* a refresh -> 401 -> refresh loop),
|
|
10
|
+
* - the BaseAuthService driving that client,
|
|
11
|
+
* - an authenticated client (access token attach + auto-refresh on 401),
|
|
12
|
+
* - the AuthService over it,
|
|
13
|
+
* - a MiddlewareBuilder for callers wiring their own API clients.
|
|
14
|
+
*/
|
|
15
|
+
export function createAuth(config, fetchImpl = fetch) {
|
|
16
|
+
const issuer = normalizeIssuer(config.issuer);
|
|
17
|
+
const normalizedConfig = { ...config, issuer };
|
|
18
|
+
const tokenClient = createClient({ baseUrl: issuer });
|
|
19
|
+
const baseAuthService = new BaseAuthService(tokenClient, normalizedConfig);
|
|
20
|
+
const middlewareBuilder = new MiddlewareBuilder(baseAuthService, fetchImpl);
|
|
21
|
+
const authClient = new ClientBuilder()
|
|
22
|
+
.withMiddleware(...middlewareBuilder.buildDefault())
|
|
23
|
+
.build({ baseUrl: issuer });
|
|
24
|
+
const authService = new AuthService(baseAuthService, authClient, normalizedConfig);
|
|
25
|
+
return {
|
|
26
|
+
authService,
|
|
27
|
+
baseAuthService,
|
|
28
|
+
middlewareBuilder,
|
|
29
|
+
callbackPath: pathnameOf(config.redirectUri),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { HttpAuthClient, BaseAuthService as IBaseAuthService } from "../../../v2/pkg/types";
|
|
1
|
+
import { HttpAuthClient, BaseAuthService as IBaseAuthService, ZitadelConfig } from "../../../v2/pkg/types/index.js";
|
|
2
2
|
export declare class BaseAuthService implements IBaseAuthService {
|
|
3
3
|
private httpAuthClient;
|
|
4
|
+
private config;
|
|
4
5
|
private refreshPromise;
|
|
5
|
-
constructor(httpAuthClient: HttpAuthClient);
|
|
6
|
-
/**
|
|
7
|
-
* @description Refresh access token
|
|
8
|
-
* @returns `accessToken` -> refresh ok. `null` -> refresh failed
|
|
9
|
-
**/
|
|
6
|
+
constructor(httpAuthClient: HttpAuthClient, config: ZitadelConfig);
|
|
10
7
|
private doRefresh;
|
|
11
8
|
Refresh(): Promise<string | null>;
|
|
12
9
|
RequestWithAccessToken(req: Request, token?: string | null): Request;
|
|
@@ -1,29 +1,57 @@
|
|
|
1
|
-
import { Storage, StorageKey } from "../../../v2/pkg/localStorage";
|
|
1
|
+
import { Storage, StorageKey } from "../../../v2/pkg/localStorage/index.js";
|
|
2
2
|
export class BaseAuthService {
|
|
3
3
|
httpAuthClient;
|
|
4
|
-
|
|
5
|
-
// @Returns the new access token or null
|
|
4
|
+
config;
|
|
6
5
|
refreshPromise = null;
|
|
7
|
-
constructor(httpAuthClient) {
|
|
6
|
+
constructor(httpAuthClient, config) {
|
|
8
7
|
this.httpAuthClient = httpAuthClient;
|
|
8
|
+
this.config = config;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
* @description Refresh access token
|
|
12
|
-
* @returns `accessToken` -> refresh ok. `null` -> refresh failed
|
|
13
|
-
**/
|
|
14
10
|
async doRefresh() {
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
17
|
-
console.error('Refresh error:', error);
|
|
18
|
-
Storage.Delete(StorageKey.ACCESS_TOKEN);
|
|
11
|
+
const refreshToken = Storage.Get(StorageKey.REFRESH_TOKEN);
|
|
12
|
+
if (!refreshToken) {
|
|
19
13
|
return null;
|
|
20
14
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
const body = new URLSearchParams({
|
|
16
|
+
grant_type: 'refresh_token',
|
|
17
|
+
refresh_token: refreshToken,
|
|
18
|
+
client_id: this.config.clientId,
|
|
19
|
+
});
|
|
20
|
+
const scopes = this.config.scopes;
|
|
21
|
+
if (scopes && scopes.length > 0) {
|
|
22
|
+
body.set('scope', scopes.join(' '));
|
|
24
23
|
}
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const { data, error } = await this.httpAuthClient.POST('/oauth/v2/token', {
|
|
25
|
+
body: body,
|
|
26
|
+
bodySerializer: () => body.toString(),
|
|
27
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
28
|
+
});
|
|
29
|
+
if (error || !data?.access_token) {
|
|
30
|
+
// Compare-and-clear: another tab may have already rotated the token while
|
|
31
|
+
// this request was in flight. Only drop stored credentials when they are
|
|
32
|
+
// still the ones we attempted with, and only for a hard auth failure.
|
|
33
|
+
const stored = Storage.Get(StorageKey.REFRESH_TOKEN);
|
|
34
|
+
const isInvalidGrant = error?.error === 'invalid_grant';
|
|
35
|
+
if (isInvalidGrant && stored === refreshToken) {
|
|
36
|
+
Storage.Delete(StorageKey.ACCESS_TOKEN);
|
|
37
|
+
Storage.Delete(StorageKey.REFRESH_TOKEN);
|
|
38
|
+
Storage.Delete(StorageKey.ID_TOKEN);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (stored && stored !== refreshToken) {
|
|
42
|
+
// A concurrent refresh succeeded; reuse the freshly stored access token.
|
|
43
|
+
return Storage.Get(StorageKey.ACCESS_TOKEN);
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
Storage.Set(StorageKey.ACCESS_TOKEN, data.access_token);
|
|
48
|
+
if (data.refresh_token) {
|
|
49
|
+
Storage.Set(StorageKey.REFRESH_TOKEN, data.refresh_token);
|
|
50
|
+
}
|
|
51
|
+
if (data.id_token) {
|
|
52
|
+
Storage.Set(StorageKey.ID_TOKEN, data.id_token);
|
|
53
|
+
}
|
|
54
|
+
return data.access_token;
|
|
27
55
|
}
|
|
28
56
|
async Refresh() {
|
|
29
57
|
if (!this.refreshPromise) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { BaseAuthService } from "../../../v2/auth/service/baseService";
|
|
2
|
-
import { AuthService } from "../../../v2/auth/service/service";
|
|
1
|
+
import { BaseAuthService } from "../../../v2/auth/service/baseService.js";
|
|
2
|
+
import { AuthService } from "../../../v2/auth/service/service.js";
|
|
3
3
|
export { AuthService, BaseAuthService };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { BaseAuthService } from "../../../v2/auth/service/baseService";
|
|
2
|
-
import { AuthService } from "../../../v2/auth/service/service";
|
|
1
|
+
import { BaseAuthService } from "../../../v2/auth/service/baseService.js";
|
|
2
|
+
import { AuthService } from "../../../v2/auth/service/service.js";
|
|
3
3
|
export { AuthService, BaseAuthService };
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { BaseAuthService, GetMeResponse, HttpAuthClient, AuthService as IAuthService } from "../../../v2/pkg/types";
|
|
1
|
+
import { BaseAuthService, GetMeResponse, HandleCallbackResult, HttpAuthClient, AuthService as IAuthService, ZitadelConfig } from "../../../v2/pkg/types/index.js";
|
|
2
|
+
import { components } from "@dehwyyy/auth/v2/pkg/types/schema";
|
|
3
|
+
type UserInfo = components["schemas"]["UserInfo"];
|
|
2
4
|
export declare class AuthService implements IAuthService {
|
|
3
|
-
private
|
|
5
|
+
private baseAuthService;
|
|
4
6
|
private authClient;
|
|
5
|
-
private
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
private config;
|
|
8
|
+
private issuer;
|
|
9
|
+
private callbackByCode;
|
|
10
|
+
constructor(baseAuthService: BaseAuthService, authClient: HttpAuthClient, config: ZitadelConfig);
|
|
11
|
+
private scopes;
|
|
12
|
+
Login(returnTo?: string): Promise<void>;
|
|
13
|
+
HandleCallback(): Promise<HandleCallbackResult>;
|
|
14
|
+
private exchange;
|
|
15
|
+
GetMe(verbose?: boolean): Promise<GetMeResponse | null>;
|
|
8
16
|
Logout(): Promise<void>;
|
|
9
|
-
|
|
17
|
+
static mapUserInfo(claims: UserInfo, verbose: boolean): GetMeResponse;
|
|
10
18
|
}
|
|
19
|
+
export {};
|
|
@@ -1,48 +1,162 @@
|
|
|
1
|
-
import { Storage, StorageKey } from "../../../v2/pkg/localStorage";
|
|
1
|
+
import { SessionKey, SessionStore, Storage, StorageKey } from "../../../v2/pkg/localStorage/index.js";
|
|
2
|
+
import { CallbackError } from "../../../v2/pkg/errors/index.js";
|
|
3
|
+
import { challengeS256, generateState, generateVerifier } from "../../../v2/pkg/pkce/index.js";
|
|
4
|
+
import { normalizeIssuer, sanitizeReturnTo } from "../../../v2/pkg/url/index.js";
|
|
5
|
+
const ROLES_CLAIM = "urn:zitadel:iam:org:project:roles";
|
|
6
|
+
const DEFAULT_SCOPES = [
|
|
7
|
+
"openid",
|
|
8
|
+
"profile",
|
|
9
|
+
"email",
|
|
10
|
+
"offline_access",
|
|
11
|
+
"urn:zitadel:iam:org:projects:roles",
|
|
12
|
+
"urn:zitadel:iam:user:metadata",
|
|
13
|
+
];
|
|
2
14
|
export class AuthService {
|
|
3
|
-
|
|
15
|
+
baseAuthService;
|
|
4
16
|
authClient;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
17
|
+
config;
|
|
18
|
+
issuer;
|
|
19
|
+
callbackByCode = new Map();
|
|
20
|
+
constructor(baseAuthService, authClient, config) {
|
|
21
|
+
this.baseAuthService = baseAuthService;
|
|
8
22
|
this.authClient = authClient;
|
|
9
|
-
this.
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.issuer = normalizeIssuer(config.issuer);
|
|
10
25
|
}
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
scopes() {
|
|
27
|
+
return (this.config.scopes ?? DEFAULT_SCOPES).join(" ");
|
|
13
28
|
}
|
|
14
|
-
async
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
async Login(returnTo) {
|
|
30
|
+
const verifier = generateVerifier();
|
|
31
|
+
const state = generateState();
|
|
32
|
+
const challenge = await challengeS256(verifier);
|
|
33
|
+
SessionStore.Set(SessionKey.PKCE_VERIFIER, verifier);
|
|
34
|
+
SessionStore.Set(SessionKey.PKCE_STATE, state);
|
|
35
|
+
if (returnTo) {
|
|
36
|
+
SessionStore.Set(SessionKey.RETURN_TO, returnTo);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
SessionStore.Delete(SessionKey.RETURN_TO);
|
|
40
|
+
}
|
|
41
|
+
const params = new URLSearchParams({
|
|
42
|
+
client_id: this.config.clientId,
|
|
43
|
+
redirect_uri: this.config.redirectUri,
|
|
44
|
+
response_type: "code",
|
|
45
|
+
scope: this.scopes(),
|
|
46
|
+
code_challenge: challenge,
|
|
47
|
+
code_challenge_method: "S256",
|
|
48
|
+
state,
|
|
49
|
+
});
|
|
50
|
+
location.assign(`${this.issuer}/oauth/v2/authorize?${params.toString()}`);
|
|
51
|
+
}
|
|
52
|
+
HandleCallback() {
|
|
53
|
+
const search = new URLSearchParams(location.search);
|
|
54
|
+
const code = search.get("code");
|
|
55
|
+
if (!code) {
|
|
56
|
+
return Promise.reject(new CallbackError("code_missing", "authorization code missing in callback"));
|
|
57
|
+
}
|
|
58
|
+
// Single-flight per authorization code: a double invocation (Vue dev double
|
|
59
|
+
// mount, manual refresh) reuses the in-flight/resolved exchange instead of
|
|
60
|
+
// re-running it and tripping a "state mismatch" on cleared session state.
|
|
61
|
+
const existing = this.callbackByCode.get(code);
|
|
62
|
+
if (existing) {
|
|
63
|
+
return existing;
|
|
64
|
+
}
|
|
65
|
+
const promise = this.exchange(code, search.get("state"));
|
|
66
|
+
this.callbackByCode.set(code, promise);
|
|
67
|
+
return promise;
|
|
68
|
+
}
|
|
69
|
+
async exchange(code, state) {
|
|
70
|
+
const storedState = SessionStore.Get(SessionKey.PKCE_STATE);
|
|
71
|
+
const verifier = SessionStore.Get(SessionKey.PKCE_VERIFIER);
|
|
72
|
+
const returnTo = sanitizeReturnTo(SessionStore.Get(SessionKey.RETURN_TO));
|
|
73
|
+
try {
|
|
74
|
+
if (!state || state !== storedState) {
|
|
75
|
+
throw new CallbackError("state_mismatch", "state mismatch in callback");
|
|
76
|
+
}
|
|
77
|
+
if (!verifier) {
|
|
78
|
+
throw new CallbackError("verifier_missing", "pkce verifier missing in callback");
|
|
79
|
+
}
|
|
80
|
+
const body = new URLSearchParams({
|
|
81
|
+
grant_type: "authorization_code",
|
|
82
|
+
code,
|
|
83
|
+
redirect_uri: this.config.redirectUri,
|
|
84
|
+
client_id: this.config.clientId,
|
|
85
|
+
code_verifier: verifier,
|
|
86
|
+
});
|
|
87
|
+
const { data, error } = await this.authClient.POST("/oauth/v2/token", {
|
|
88
|
+
body: body,
|
|
89
|
+
bodySerializer: () => body.toString(),
|
|
90
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
91
|
+
});
|
|
92
|
+
if (error || !data?.access_token) {
|
|
93
|
+
throw new CallbackError("exchange_failed", "token exchange failed");
|
|
94
|
+
}
|
|
95
|
+
Storage.Set(StorageKey.ACCESS_TOKEN, data.access_token);
|
|
96
|
+
if (data.refresh_token) {
|
|
97
|
+
Storage.Set(StorageKey.REFRESH_TOKEN, data.refresh_token);
|
|
98
|
+
}
|
|
99
|
+
if (data.id_token) {
|
|
100
|
+
Storage.Set(StorageKey.ID_TOKEN, data.id_token);
|
|
101
|
+
}
|
|
102
|
+
const me = await this.GetMe(true);
|
|
103
|
+
return { me, returnTo };
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
SessionStore.Delete(SessionKey.PKCE_VERIFIER);
|
|
107
|
+
SessionStore.Delete(SessionKey.PKCE_STATE);
|
|
108
|
+
SessionStore.Delete(SessionKey.RETURN_TO);
|
|
20
109
|
}
|
|
21
110
|
}
|
|
22
|
-
async GetMe(
|
|
111
|
+
async GetMe(verbose = false) {
|
|
23
112
|
let token = Storage.Get(StorageKey.ACCESS_TOKEN);
|
|
24
113
|
if (!token) {
|
|
25
|
-
token = await this.
|
|
114
|
+
token = await this.baseAuthService.Refresh();
|
|
26
115
|
if (!token) {
|
|
27
116
|
return null;
|
|
28
117
|
}
|
|
29
118
|
}
|
|
30
|
-
const { data, error } = await this.authClient.GET("/
|
|
31
|
-
params: {
|
|
32
|
-
query: {
|
|
33
|
-
app,
|
|
34
|
-
verbose: verbose ? "all" : "none",
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
119
|
+
const { data, error } = await this.authClient.GET("/oidc/v1/userinfo");
|
|
38
120
|
if (error) {
|
|
39
|
-
console.error(error);
|
|
40
121
|
return null;
|
|
41
122
|
}
|
|
42
|
-
if (!data || !data.
|
|
43
|
-
console.warn("Failed to get user info:", data);
|
|
123
|
+
if (!data || !data.sub) {
|
|
44
124
|
return null;
|
|
45
125
|
}
|
|
46
|
-
return data;
|
|
126
|
+
return AuthService.mapUserInfo(data, verbose);
|
|
127
|
+
}
|
|
128
|
+
async Logout() {
|
|
129
|
+
const idToken = Storage.Get(StorageKey.ID_TOKEN);
|
|
130
|
+
Storage.Delete(StorageKey.ACCESS_TOKEN);
|
|
131
|
+
Storage.Delete(StorageKey.REFRESH_TOKEN);
|
|
132
|
+
Storage.Delete(StorageKey.ID_TOKEN);
|
|
133
|
+
const params = new URLSearchParams();
|
|
134
|
+
if (idToken) {
|
|
135
|
+
params.set("id_token_hint", idToken);
|
|
136
|
+
}
|
|
137
|
+
const postLogout = this.config.postLogoutRedirectUri;
|
|
138
|
+
if (postLogout) {
|
|
139
|
+
params.set("post_logout_redirect_uri", postLogout);
|
|
140
|
+
}
|
|
141
|
+
location.assign(`${this.issuer}/oidc/v1/end_session?${params.toString()}`);
|
|
142
|
+
}
|
|
143
|
+
static mapUserInfo(claims, verbose) {
|
|
144
|
+
const rolesClaim = claims[ROLES_CLAIM] ?? {};
|
|
145
|
+
const roles = Object.keys(rolesClaim);
|
|
146
|
+
const me = {
|
|
147
|
+
user_id: claims.sub,
|
|
148
|
+
roles,
|
|
149
|
+
active: true,
|
|
150
|
+
};
|
|
151
|
+
if (verbose) {
|
|
152
|
+
me.info = {
|
|
153
|
+
username: claims.preferred_username,
|
|
154
|
+
email: claims.email,
|
|
155
|
+
email_verified: claims.email_verified,
|
|
156
|
+
name: claims.name,
|
|
157
|
+
picture: claims.picture,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return me;
|
|
47
161
|
}
|
|
48
162
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { ClientBuilder } from "../../../v2/client/builder/builder";
|
|
1
|
+
import { ClientBuilder } from "../../../v2/client/builder/builder.js";
|
|
2
2
|
export { ClientBuilder };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { ClientBuilder } from "../../../v2/client/builder/builder";
|
|
1
|
+
import { ClientBuilder } from "../../../v2/client/builder/builder.js";
|
|
2
2
|
export { ClientBuilder };
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import createClient
|
|
1
|
+
import createClient from "openapi-fetch";
|
|
2
2
|
type Client = ReturnType<typeof createClient<{}>>;
|
|
3
3
|
export declare class ClientInstance {
|
|
4
4
|
private static map;
|
|
5
5
|
New<T extends Client>(key: string, clientFactory: () => T): T;
|
|
6
6
|
}
|
|
7
|
-
export declare class ClientBuilder<OpenAPIClientDefinitions extends {}> {
|
|
8
|
-
private middlewares;
|
|
9
|
-
constructor();
|
|
10
|
-
withMiddleware(...middlewares: OpenAPIMiddleware[]): this;
|
|
11
|
-
build(opts: ClientOptions): ReturnType<typeof createClient<OpenAPIClientDefinitions>>;
|
|
12
|
-
}
|
|
13
7
|
export {};
|