@drax/identity-vue 3.68.0 → 3.70.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 CHANGED
@@ -89,29 +89,89 @@ Removing a header directly is only a transport operation and intentionally does
89
89
  clear the store: use session/store actions for credential changes. Direct `$patch` is
90
90
  not a session API; normal request maintenance re-reads persisted state.
91
91
 
92
- External providers must mark the credential exchange as independent using the existing
93
- HTTP option, even if the old Drax session is recoverable:
92
+ External login should use the public workflow:
94
93
 
95
94
  ```ts
96
- const response = await this.httpClient.post('/api/google/login', {credential}, {
97
- timeout: 120000,
98
- removeHeaders: ['Authorization'],
99
- })
100
- await loginWithToken(response.accessToken)
95
+ useAuth().loginWithSession(
96
+ operation: (rememberMe: boolean) => Promise<ILoginResponse>,
97
+ rememberMe = false,
98
+ ): Promise<void>
101
99
  ```
102
100
 
103
- Do not remove/restore global headers or set the returned token manually before
104
- `loginWithToken`. The request option excludes Authorization case-insensitively and
105
- skips the session preflight without changing shared state. Its 401 is the external
106
- credential's rejection, not a reason to clear the existing Drax session. No global
107
- response interceptor performs logout. This does not disable browser CORS preflight.
101
+ `ILoginResponse` is the existing exported type from `@drax/identity-front`:
102
+ `{accessToken: string, refreshToken?: string, refreshExpiresAt?: string}`.
103
+ The workflow clears the previous credentials/profile, captures the operation epoch,
104
+ then calls `operation` with the effective frontend flag + explicit opt-in. It confirms
105
+ the session, synchronizes headers and loads the profile using the same path as standard
106
+ login. Failures reject the promise; only navigate after successful completion.
107
+ Discarded or unwanted refresh credentials are revoked best-effort. A profile failure
108
+ clears the current attempted login; offline revocation cannot be guaranteed.
109
+
110
+ **Start the entire async exchange inside the callback.** Passing a promise started
111
+ before the workflow, or awaiting the response then calling `loginWithToken`, cannot
112
+ protect against logout/new login while that request is pending. Legacy
113
+ `loginWithToken(string)` remains available for access-token-only callers; it does not
114
+ accept or persist refresh credentials.
115
+
116
+ Example consumer component (the consumer's Google integration supplies
117
+ `obtainGoogleCredential`; no Google SDK is added to Drax):
118
+
119
+ ```vue
120
+ <script setup lang="ts">
121
+ import {ref} from 'vue'
122
+ import {useRouter} from 'vue-router'
123
+ import {HttpRestClientFactory} from '@drax/common-front'
124
+ import type {ILoginResponse} from '@drax/identity-front'
125
+ import {IdentityRememberMe, useAuth} from '@drax/identity-vue'
126
+ import {obtainGoogleCredential} from './googleCredential'
127
+
128
+ const rememberMe = ref(false)
129
+ const loading = ref(false)
130
+ const {loginWithSession} = useAuth()
131
+ const router = useRouter()
132
+
133
+ async function loginGoogle() {
134
+ loading.value = true
135
+ try {
136
+ await loginWithSession(async effectiveRememberMe => {
137
+ const credential = await obtainGoogleCredential()
138
+ return await HttpRestClientFactory.getInstance().post('/api/google/login', {
139
+ credential, rememberMe: effectiveRememberMe,
140
+ }, {removeHeaders: ['Authorization']}) as ILoginResponse
141
+ }, rememberMe.value)
142
+ await router.push('/') // Adapt to the consumer's authorized destination.
143
+ } catch {
144
+ // Show the consumer's localized error; never log credentials or headers.
145
+ } finally {
146
+ loading.value = false
147
+ }
148
+ }
149
+ </script>
150
+
151
+ <template>
152
+ <IdentityRememberMe v-model="rememberMe" />
153
+ <v-btn :loading="loading" :disabled="loading" @click="loginGoogle">Google</v-btn>
154
+ </template>
155
+ ```
156
+
157
+ This checkbox starts unchecked and hides itself when the feature is disabled. It is
158
+ independent from the private checkbox inside `IdentityLogin`; an external button must
159
+ supply its own explicit opt-in. The callback receives a snapshot, not a later checkbox
160
+ value. Do not write tokens to Pinia/localStorage or manipulate Authorization manually.
161
+
162
+ Use `removeHeaders: ['Authorization']` on the external exchange: it excludes stale
163
+ bearer credentials and skips Drax session preflight for that request, without mutating
164
+ global headers or disabling browser CORS preflight. There is no second refresh/retry
165
+ mechanism. The backend must verify the external identity before issuing any Drax
166
+ session; see `identity-back/docs/remember-me.md`.
108
167
 
109
168
  Consumer changes:
110
169
 
111
170
  - **GoogleProvider:** add `removeHeaders: ['Authorization']` to the login POST and
112
- remove the redundant `setHttpClientToken(response.accessToken)` call; keep
113
- `await loginWithToken(response.accessToken)` after a successful exchange. See
114
- Zuite's `src/modules/base/providers/GoogleProvider.ts`.
171
+ remove redundant `setHttpClientToken` calls and return the full `ILoginResponse`.
172
+ Invoke that provider inside `loginWithSession(effectiveRememberMe =>
173
+ provider.login(credential, effectiveRememberMe), rememberMe)`; obtain the credential
174
+ inside the callback too if that step is asynchronous. Do not start the request first.
115
175
  - **LoginPage:** delete the workaround that checks invalid access tokens and calls
116
176
  `logout()`. Session cleanup belongs to maintenance, not a page (logout also navigates).
117
177
  - **Bootstrap:** retain `app.use(pinia); await setupAuthSession(); app.use(router)`;
@@ -132,6 +192,35 @@ consumer-owned; Drax tests use a deterministic external verifier, not Google's s
132
192
  REST request-time maintenance requires the shared client's optional `setBeforeRequest`;
133
193
  GraphQL and custom clients without that hook do not get automatic preflight renewal.
134
194
 
195
+ ### External remember-me rollout and security
196
+
197
+ This change requires new releases of **`@drax/identity-back` and
198
+ `@drax/identity-vue`**. No new type or runtime changes are needed in identity-front,
199
+ identity-share or common-front for this extension, but consumers must retain compatible
200
+ remember-me APIs and the shared REST preflight (avoid duplicate singleton packages).
201
+ No publication/version is implied by this source change. Deploy the compatible backend
202
+ and its verified external endpoint first, then update/rebuild the frontend coherently.
203
+ The user must log in again and explicitly check the option; existing ordinary sessions
204
+ are not upgraded.
205
+
206
+ Keep `DRAX_REMEMBER_ME_ENABLED=true`, optional
207
+ `DRAX_REFRESH_TOKEN_TTL_SECONDS=2592000` on the backend and
208
+ `VITE_DRAX_REMEMBER_ME_ENABLED=true` at frontend build time. Refresh has an absolute
209
+ TTL, uses the user's **currently assigned database tenant**, and does not extend JWT
210
+ lifetime. A tenant selected with switchTenant must be selected again after renewal.
211
+
212
+ Storage contains sensitive credentials in **localStorage**, not HttpOnly cookies.
213
+ Use HTTPS and XSS protections. Web Locks coordinates refresh across tabs when available;
214
+ without it, single-flight is per tab only. Disabling the backend flag blocks new
215
+ remembered sessions/refresh; disabling the frontend flag prevents opt-in and maintenance
216
+ strips legacy local refresh credentials. Neither flag revokes already-issued JWTs or
217
+ all server refresh records. Re-enabling can make unexpired, non-revoked refresh tokens
218
+ usable again. Logout revokes the latest known Drax refresh token best-effort.
219
+
220
+ Drax refresh tokens renew **Drax sessions only**. They are not Google's OAuth refresh
221
+ or access tokens and do not grant Gmail/Calendar access or offline Google API consent.
222
+ Keep Google API authorization and its token storage separate.
223
+
135
224
  ## Registro con creación de tenant (opcional, REST)
136
225
 
137
226
  En el **backend**:
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "3.68.0",
6
+ "version": "3.70.0",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "module": "./src/index.ts",
@@ -22,7 +22,7 @@
22
22
  "@drax/common-vue": "^3.68.0",
23
23
  "@drax/crud-front": "^3.58.0",
24
24
  "@drax/crud-share": "^3.65.0",
25
- "@drax/crud-vue": "^3.68.0",
25
+ "@drax/crud-vue": "^3.70.0",
26
26
  "@drax/identity-front": "^3.68.0",
27
27
  "@drax/identity-share": "^3.67.0"
28
28
  },
@@ -49,5 +49,5 @@
49
49
  "vue-tsc": "^3.2.4",
50
50
  "vuetify": "^3.11.8"
51
51
  },
52
- "gitHead": "c8300dd678d4048416da13f9065d6749e715c3bf"
52
+ "gitHead": "70cfd64361d889fbeaa84ad258d498767f2c8c3a"
53
53
  }
@@ -1,6 +1,6 @@
1
1
  import {useAuthStore} from "../stores/AuthStore";
2
2
  import {AuthHelper, AuthSystemFactory} from "@drax/identity-front";
3
- import type {AuthSystem} from "@drax/identity-front";
3
+ import type {AuthSystem, ILoginResponse} from "@drax/identity-front";
4
4
  import {useRouter} from 'vue-router'
5
5
  import {authSessionRevision, clearAuthSession, isCurrentAuthOperation, isCurrentAuthSession, isRememberMeEnabled, rereadSession, revokeSessionToken, syncAuthHeaders} from './setupAuthSession'
6
6
 
@@ -11,14 +11,13 @@ export function useAuth() {
11
11
 
12
12
  const authSystem: AuthSystem = AuthSystemFactory.getInstance()
13
13
 
14
- const login = async (username: string, password: string, rememberMe = false) => {
14
+ /** Start the external credential exchange inside operation so logout/new login can cancel its result. */
15
+ const loginWithSession = async (operation: (rememberMe: boolean) => Promise<ILoginResponse>, rememberMe = false) => {
15
16
  clearAuthSession(authStore)
16
17
  const revision = authSessionRevision(authStore)
17
18
  const version = authStore.sessionVersion
18
19
  const remembered = isRememberMeEnabled() && rememberMe
19
- const result = remembered
20
- ? await authSystem.login(username, password, true)
21
- : await authSystem.login(username, password)
20
+ const result = await operation(remembered)
22
21
  if (authSessionRevision(authStore) !== revision || !isCurrentAuthOperation(authStore, version)) {
23
22
  syncAuthHeaders(authStore)
24
23
  if (result.refreshToken) await revokeSessionToken(result.refreshToken).catch(() => {})
@@ -26,6 +25,8 @@ export function useAuth() {
26
25
  }
27
26
  if (remembered) authStore.setSession(result)
28
27
  else authStore.setAccessToken(result.accessToken)
28
+ // A server returning refresh credentials without opt-in must not leave an orphan session.
29
+ if (!remembered && result.refreshToken) void revokeSessionToken(result.refreshToken).catch(() => {})
29
30
  syncAuthHeaders(authStore)
30
31
  try {
31
32
  await fetchAuthUser()
@@ -35,11 +36,18 @@ export function useAuth() {
35
36
  const token = authStore.refreshToken
36
37
  clearAuthSession(authStore)
37
38
  if (token) await revokeSessionToken(token).catch(() => {})
39
+ } else if (result.refreshToken) {
40
+ await revokeSessionToken(result.refreshToken).catch(() => {})
38
41
  }
39
42
  throw error
40
43
  }
41
44
  }
42
45
 
46
+ const login = async (username: string, password: string, rememberMe = false) => {
47
+ await loginWithSession(remembered => remembered
48
+ ? authSystem.login(username, password, true)
49
+ : authSystem.login(username, password), rememberMe)
50
+ }
43
51
 
44
52
  const switchTenant = async (tenantId: string) => {
45
53
  const {accessToken} = await authSystem.switchTenant(tenantId)
@@ -150,7 +158,7 @@ export function useAuth() {
150
158
  }
151
159
 
152
160
  return {
153
- login, logout, loginWithToken,
161
+ login, logout, loginWithToken, loginWithSession,
154
162
  tokenIsValid, hasPermission,
155
163
  isAuthenticated, fetchAuthUser,
156
164
  changeOwnPassword, changeAvatar, passwordPolicy,